Merge branch 'master' into openshift2

Conflicts:
bin/setup_heroku

Andrew Cantino 9 gadi atpakaļ
vecāks
revīzija
225d73aca3
113 mainītis faili ar 2079 papildinājumiem un 562 dzēšanām
  1. 10 2
      .env.example
  2. 9 1
      Gemfile
  3. 50 4
      Gemfile.lock
  4. 25 0
      Guardfile
  5. 1 0
      app/assets/javascripts/application.js.coffee
  6. 76 0
      app/assets/javascripts/components/form_configurable.js.coffee
  7. 12 10
      app/assets/javascripts/components/worker-checker.js.coffee
  8. 5 5
      app/assets/javascripts/pages/agent-edit-page.js.coffee
  9. 21 0
      app/assets/javascripts/pages/agent-show-page.js.coffee
  10. 29 18
      app/assets/stylesheets/application.css.scss.erb
  11. 14 0
      app/assets/stylesheets/tables.css.scss
  12. 34 9
      app/concerns/agent_controller_concern.rb
  13. 35 0
      app/concerns/dropbox_concern.rb
  14. 66 0
      app/concerns/form_configurable.rb
  15. 3 3
      app/concerns/twitter_concern.rb
  16. 38 4
      app/controllers/agents_controller.rb
  17. 1 1
      app/controllers/application_controller.rb
  18. 1 1
      app/controllers/events_controller.rb
  19. 2 2
      app/controllers/home_controller.rb
  20. 1 1
      app/controllers/jobs_controller.rb
  21. 1 1
      app/controllers/logs_controller.rb
  22. 15 0
      app/controllers/omniauth_callbacks_controller.rb
  23. 1 1
      app/controllers/scenarios_controller.rb
  24. 1 10
      app/controllers/services_controller.rb
  25. 2 1
      app/controllers/web_requests_controller.rb
  26. 27 7
      app/controllers/worker_status_controller.rb
  27. 1 1
      app/helpers/agent_helper.rb
  28. 63 8
      app/helpers/application_helper.rb
  29. 6 2
      app/models/agent.rb
  30. 1 1
      app/models/agent_log.rb
  31. 23 12
      app/models/agents/basecamp_agent.rb
  32. 47 0
      app/models/agents/commander_agent.rb
  33. 58 0
      app/models/agents/dropbox_file_url_agent.rb
  34. 114 0
      app/models/agents/dropbox_watch_agent.rb
  35. 33 4
      app/models/agents/hipchat_agent.rb
  36. 8 0
      app/models/agents/imap_folder_agent.rb
  37. 31 18
      app/models/agents/mqtt_agent.rb
  38. 12 10
      app/models/agents/rss_agent.rb
  39. 1 1
      app/models/agents/scheduler_agent.rb
  40. 2 1
      app/models/agents/user_location_agent.rb
  41. 1 1
      app/models/agents/website_agent.rb
  42. 1 1
      app/models/scenario_import.rb
  43. 1 3
      app/models/service.rb
  44. 44 0
      app/presenters/form_configurable_agent_presenter.rb
  45. 9 9
      app/views/agents/_action_menu.html.erb
  46. 4 16
      app/views/agents/_form.html.erb
  47. 1 1
      app/views/agents/_oauth_dropdown.html.erb
  48. 27 0
      app/views/agents/_options.erb
  49. 6 5
      app/views/agents/edit.html.erb
  50. 3 3
      app/views/agents/index.html.erb
  51. 5 3
      app/views/agents/new.html.erb
  52. 9 2
      app/views/agents/show.html.erb
  53. 4 4
      app/views/devise/confirmations/new.html.erb
  54. 2 2
      app/views/devise/mailer/confirmation_instructions.html.erb
  55. 2 2
      app/views/devise/mailer/reset_password_instructions.html.erb
  56. 2 2
      app/views/devise/mailer/unlock_instructions.html.erb
  57. 7 7
      app/views/devise/passwords/edit.html.erb
  58. 4 4
      app/views/devise/passwords/new.html.erb
  59. 22 18
      app/views/devise/registrations/edit.html.erb
  60. 14 13
      app/views/devise/registrations/new.html.erb
  61. 10 10
      app/views/devise/sessions/new.html.erb
  62. 2 8
      app/views/devise/shared/_links.erb
  63. 5 5
      app/views/devise/unlocks/new.html.erb
  64. 1 1
      app/views/diagrams/show.html.erb
  65. 5 5
      app/views/events/index.html.erb
  66. 1 1
      app/views/events/show.html.erb
  67. 7 7
      app/views/layouts/_navigation.html.erb
  68. 3 1
      app/views/logs/index.html.erb
  69. 2 2
      app/views/scenario_imports/new.html.erb
  70. 2 2
      app/views/scenarios/edit.html.erb
  71. 2 2
      app/views/scenarios/index.html.erb
  72. 2 2
      app/views/scenarios/new.html.erb
  73. 1 1
      app/views/scenarios/share.html.erb
  74. 6 6
      app/views/scenarios/show.html.erb
  75. 3 3
      app/views/services/index.html.erb
  76. 2 2
      app/views/user_credentials/edit.html.erb
  77. 1 1
      app/views/user_credentials/index.html.erb
  78. 2 2
      app/views/user_credentials/new.html.erb
  79. 2 2
      bin/schedule.rb
  80. 1 1
      bin/threaded.rb
  81. 1 1
      config/application.rb
  82. 1 1
      config/boot.rb
  83. 6 0
      config/initializers/delayed_job.rb
  84. 54 28
      config/initializers/devise.rb
  85. 55 52
      config/locales/devise.en.yml
  86. 5 3
      config/routes.rb
  87. 1 1
      deployment/site-cookbooks/huginn_production/files/default/unicorn.rb
  88. 1 1
      doc/deployment/unicorn/production.rb
  89. 1 1
      docker/README.md
  90. 2 2
      lib/huginn_scheduler.rb
  91. 56 0
      spec/concerns/form_configurable_spec.rb
  92. 40 0
      spec/controllers/agents_controller_spec.rb
  93. 26 0
      spec/controllers/omniauth_callbacks_controller_spec.rb
  94. 1 1
      spec/controllers/scenarios_controller_spec.rb
  95. 0 18
      spec/controllers/services_controller_spec.rb
  96. 2 0
      spec/env.test
  97. 62 13
      spec/helpers/application_helper_spec.rb
  98. 3 3
      spec/lib/huginn_scheduler_spec.rb
  99. 2 2
      spec/models/agent_log_spec.rb
  100. 17 12
      spec/models/agent_spec.rb
  101. 28 5
      spec/models/agents/basecamp_agent_spec.rb
  102. 42 0
      spec/models/agents/commander_agent_spec.rb
  103. 74 0
      spec/models/agents/dropbox_file_url_agent_spec.rb
  104. 173 0
      spec/models/agents/dropbox_watch_agent_spec.rb
  105. 25 0
      spec/models/agents/hipchat_agent_spec.rb
  106. 8 2
      spec/models/agents/mqtt_agent_spec.rb
  107. 4 0
      spec/models/agents/rss_agent_spec.rb
  108. 59 105
      spec/models/agents/scheduler_agent_spec.rb
  109. 1 1
      spec/models/service_spec.rb
  110. 40 0
      spec/presenters/form_configurable_agent_presenter_spec.rb
  111. 30 11
      spec/support/fake_mqtt_server.rb
  112. 111 0
      spec/support/shared_examples/agent_controller_concern.rb
  113. 40 0
      vendor/assets/javascripts/jquery.serializeObject.js

+ 10 - 2
.env.example

@@ -53,8 +53,8 @@ INVITATION_CODE=try-huginn
53 53
 
54 54
 # Outgoing email settings.  To use Gmail or Google Apps, put your Google Apps domain or gmail.com
55 55
 # as the SMTP_DOMAIN and your Gmail username and password as the SMTP_USER_NAME and SMTP_PASSWORD.
56
-# 
57
-# PLEASE NOTE: In order to enable emails locally (e.g., when not in the production Rails environment), 
56
+#
57
+# PLEASE NOTE: In order to enable emails locally (e.g., when not in the production Rails environment),
58 58
 # you must also change config.action_mailer.perform_deliveries in config/environments/development.rb.
59 59
 
60 60
 SMTP_DOMAIN=your-domain-here.com
@@ -92,6 +92,9 @@ GITHUB_OAUTH_SECRET=
92 92
 TUMBLR_OAUTH_KEY=
93 93
 TUMBLR_OAUTH_SECRET=
94 94
 
95
+DROPBOX_OAUTH_KEY=
96
+DROPBOX_OAUTH_SECRET=
97
+
95 98
 #############################
96 99
 #  AWS and Mechanical Turk  #
97 100
 #############################
@@ -132,6 +135,11 @@ ENABLE_INSECURE_AGENTS=false
132 135
 # "on the minute") is disallowed to prevent abuse of service.
133 136
 ENABLE_SECOND_PRECISION_SCHEDULE=false
134 137
 
138
+# Specify the scheduler frequency in seconds (default: 0.3).
139
+# Increasing this value will help reduce the use of system resources
140
+# at the expense of time accuracy.
141
+#SCHEDULER_FREQUENCY=0.3
142
+
135 143
 # Use Graphviz for generating diagrams instead of using Google Chart
136 144
 # Tools.  Specify a dot(1) command path built with SVG support
137 145
 # enabled.

+ 9 - 1
Gemfile

@@ -26,6 +26,10 @@ gem 'omniauth-twitter'
26 26
 gem 'tumblr_client'
27 27
 gem 'omniauth-tumblr'
28 28
 
29
+# Dropbox Agents
30
+gem 'dropbox-api'
31
+gem 'omniauth-dropbox'
32
+
29 33
 # Optional Services.
30 34
 gem 'omniauth-37signals'          # BasecampAgent
31 35
 # gem 'omniauth-github'
@@ -46,7 +50,7 @@ gem 'coffee-rails', '~> 4.0.0'
46 50
 gem 'daemons', '~> 1.1.9'
47 51
 gem 'delayed_job', '~> 4.0.0'
48 52
 gem 'delayed_job_active_record', '~> 4.0.0'
49
-gem 'devise', '~> 3.2.4'
53
+gem 'devise', '~> 3.4.0'
50 54
 gem 'em-http-request', '~> 1.1.2'
51 55
 gem 'faraday', '~> 0.9.0'
52 56
 gem 'faraday_middleware'
@@ -81,6 +85,9 @@ group :development do
81 85
   gem 'better_errors', '~> 1.1'
82 86
   gem 'binding_of_caller'
83 87
   gem 'quiet_assets'
88
+  gem 'guard'
89
+  gem 'guard-livereload'
90
+  gem 'guard-rspec'
84 91
 end
85 92
 
86 93
 group :development, :test do
@@ -92,6 +99,7 @@ group :development, :test do
92 99
   gem 'rspec', '~> 3.0'
93 100
   gem 'rspec-collection_matchers', '~> 1.0.0'
94 101
   gem 'rspec-rails', '~> 3.0.1'
102
+  gem 'rspec-html-matchers', '~> 0.6.1'
95 103
   gem 'shoulda-matchers'
96 104
   gem 'spring'
97 105
   gem 'spring-commands-rspec'

+ 50 - 4
Gemfile.lock

@@ -55,6 +55,8 @@ GEM
55 55
       rails (>= 3.1)
56 56
     buftok (0.2.0)
57 57
     builder (3.2.2)
58
+    celluloid (0.15.2)
59
+      timers (~> 1.1.0)
58 60
     chronic (0.10.2)
59 61
     coderay (1.1.0)
60 62
     coffee-rails (4.0.1)
@@ -82,10 +84,11 @@ GEM
82 84
       delayed_job (>= 3.0, < 4.1)
83 85
     delorean (2.1.0)
84 86
       chronic
85
-    devise (3.2.4)
87
+    devise (3.4.0)
86 88
       bcrypt (~> 3.0)
87 89
       orm_adapter (~> 0.1)
88 90
       railties (>= 3.2.6, < 5)
91
+      responders
89 92
       thread_safe (~> 0.1)
90 93
       warden (~> 1.2.3)
91 94
     diff-lcs (1.2.5)
@@ -95,6 +98,10 @@ GEM
95 98
     dotenv-deployment (0.0.2)
96 99
     dotenv-rails (0.11.1)
97 100
       dotenv (= 0.11.1)
101
+    dropbox-api (0.4.2)
102
+      hashie
103
+      multi_json
104
+      oauth
98 105
     em-http-request (1.1.2)
99 106
       addressable (>= 2.3.4)
100 107
       cookiejar
@@ -103,6 +110,9 @@ GEM
103 110
       http_parser.rb (>= 0.6.0)
104 111
     em-socksify (0.3.0)
105 112
       eventmachine (>= 1.0.0.beta.4)
113
+    em-websocket (0.5.1)
114
+      eventmachine (>= 0.12.9)
115
+      http_parser.rb (~> 0.6.0)
106 116
     equalizer (0.0.9)
107 117
     erector (0.10.0)
108 118
       treetop (>= 1.2.3)
@@ -129,6 +139,7 @@ GEM
129 139
     foreman (0.63.0)
130 140
       dotenv (>= 0.7)
131 141
       thor (>= 0.13.6)
142
+    formatador (0.2.5)
132 143
     geokit (1.8.5)
133 144
       multi_json (>= 1.3.2)
134 145
     geokit-rails (2.0.1)
@@ -145,6 +156,19 @@ GEM
145 156
       retriable (>= 1.4)
146 157
       signet (>= 0.5.0)
147 158
       uuidtools (>= 2.1.0)
159
+    guard (2.6.1)
160
+      formatador (>= 0.2.4)
161
+      listen (~> 2.7)
162
+      lumberjack (~> 1.0)
163
+      pry (>= 0.9.12)
164
+      thor (>= 0.18.1)
165
+    guard-livereload (2.2.0)
166
+      em-websocket (~> 0.5)
167
+      guard (~> 2.0)
168
+      multi_json (~> 1.8)
169
+    guard-rspec (4.3.1)
170
+      guard (~> 2.1)
171
+      rspec (>= 2.14, < 4.0)
148 172
     hashie (2.0.5)
149 173
     hike (1.2.3)
150 174
     hipchat (1.2.0)
@@ -171,8 +195,13 @@ GEM
171 195
     kramdown (1.3.3)
172 196
     launchy (2.4.2)
173 197
       addressable (~> 2.3)
174
-    libv8 (3.16.14.3)
198
+    libv8 (3.16.14.7)
175 199
     liquid (2.6.1)
200
+    listen (2.7.9)
201
+      celluloid (>= 0.15.2)
202
+      rb-fsevent (>= 0.9.3)
203
+      rb-inotify (>= 0.9)
204
+    lumberjack (1.0.9)
176 205
     macaddr (1.7.1)
177 206
       systemu (~> 2.6.2)
178 207
     mail (2.5.4)
@@ -184,7 +213,7 @@ GEM
184 213
     mime-types (1.25.1)
185 214
     mini_portile (0.6.0)
186 215
     minitest (5.4.0)
187
-    mqtt (0.2.0)
216
+    mqtt (0.3.1)
188 217
     multi_json (1.10.1)
189 218
     multi_xml (0.5.5)
190 219
     multipart-post (2.0.0)
@@ -206,6 +235,8 @@ GEM
206 235
     omniauth-37signals (1.0.5)
207 236
       omniauth (~> 1.0)
208 237
       omniauth-oauth2 (~> 1.0)
238
+    omniauth-dropbox (0.2.0)
239
+      omniauth-oauth (~> 1.0)
209 240
     omniauth-oauth (1.0.1)
210 241
       oauth
211 242
       omniauth (~> 1.0)
@@ -255,9 +286,14 @@ GEM
255 286
       thor (>= 0.18.1, < 2.0)
256 287
     raindrops (0.13.0)
257 288
     rake (10.3.2)
289
+    rb-fsevent (0.9.4)
290
+    rb-inotify (0.9.5)
291
+      ffi (>= 0.5.0)
258 292
     rdoc (4.1.1)
259 293
       json (~> 1.4)
260 294
     ref (1.0.5)
295
+    responders (1.1.1)
296
+      railties (>= 3.2, < 4.2)
261 297
     rest-client (1.6.8)
262 298
       mime-types (~> 1.16)
263 299
       rdoc (>= 2.4.2)
@@ -274,6 +310,9 @@ GEM
274 310
     rspec-expectations (3.0.4)
275 311
       diff-lcs (>= 1.2.0, < 2.0)
276 312
       rspec-support (~> 3.0.0)
313
+    rspec-html-matchers (0.6.1)
314
+      nokogiri (~> 1)
315
+      rspec (~> 3)
277 316
     rspec-mocks (3.0.4)
278 317
       rspec-support (~> 3.0.0)
279 318
     rspec-rails (3.0.2)
@@ -341,6 +380,7 @@ GEM
341 380
     thor (0.19.1)
342 381
     thread_safe (0.3.4)
343 382
     tilt (1.4.1)
383
+    timers (1.1.0)
344 384
     tins (1.3.2)
345 385
     treetop (1.4.15)
346 386
       polyglot
@@ -414,9 +454,10 @@ DEPENDENCIES
414 454
   delayed_job (~> 4.0.0)
415 455
   delayed_job_active_record (~> 4.0.0)
416 456
   delorean
417
-  devise (~> 3.2.4)
457
+  devise (~> 3.4.0)
418 458
   dotenv-deployment
419 459
   dotenv-rails
460
+  dropbox-api
420 461
   em-http-request (~> 1.1.2)
421 462
   faraday (~> 0.9.0)
422 463
   faraday_middleware
@@ -428,6 +469,9 @@ DEPENDENCIES
428 469
   geokit (~> 1.8.4)
429 470
   geokit-rails (~> 2.0.1)
430 471
   google-api-client
472
+  guard
473
+  guard-livereload
474
+  guard-rspec
431 475
   hipchat (~> 1.2.0)
432 476
   httparty (~> 0.13)
433 477
   jquery-rails (~> 3.1.0)
@@ -443,6 +487,7 @@ DEPENDENCIES
443 487
   nokogiri (~> 1.6.1)
444 488
   omniauth
445 489
   omniauth-37signals
490
+  omniauth-dropbox
446 491
   omniauth-tumblr
447 492
   omniauth-twitter
448 493
   pg
@@ -455,6 +500,7 @@ DEPENDENCIES
455 500
   rr
456 501
   rspec (~> 3.0)
457 502
   rspec-collection_matchers (~> 1.0.0)
503
+  rspec-html-matchers (~> 0.6.1)
458 504
   rspec-rails (~> 3.0.1)
459 505
   rturk (~> 2.12.1)
460 506
   ruby-growl (~> 4.1.0)

+ 25 - 0
Guardfile

@@ -0,0 +1,25 @@
1
+
2
+guard 'livereload' do
3
+  watch(%r{app/views/.+\.(erb|haml|slim)$})
4
+  watch(%r{app/helpers/.+\.rb})
5
+  watch(%r{public/.+\.(css|js|html)})
6
+  watch(%r{config/locales/.+\.yml})
7
+  # Rails Assets Pipeline
8
+  watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" }
9
+end
10
+
11
+guard :rspec, cmd: 'bundle exec spring rspec' do
12
+  watch(%r{^spec/.+_spec\.rb$})
13
+  watch(%r{^lib/(.+)\.rb$})     { |m| "spec/lib/#{m[1]}_spec.rb" }
14
+  watch('spec/spec_helper.rb')  { "spec" }
15
+
16
+  # Rails example
17
+  watch(%r{^app/(.+)\.rb$})                           { |m| "spec/#{m[1]}_spec.rb" }
18
+  watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$})          { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
19
+  watch(%r{^app/controllers/(.+)_(controller)\.rb$})  { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
20
+  watch(%r{^spec/support/(.+)\.rb$})                  { "spec" }
21
+  watch('config/routes.rb')                           { "spec/routing" }
22
+  watch('app/controllers/application_controller.rb')  { "spec/controllers" }
23
+  watch('spec/rails_helper.rb')                       { "spec" }
24
+end
25
+

+ 1 - 0
app/assets/javascripts/application.js.coffee

@@ -5,6 +5,7 @@
5 5
 #= require select2
6 6
 #= require json2
7 7
 #= require jquery.json-editor
8
+#= require jquery.serializeObject
8 9
 #= require latlon_and_geo
9 10
 #= require spectrum
10 11
 #= require_tree ./components

+ 76 - 0
app/assets/javascripts/components/form_configurable.js.coffee

@@ -0,0 +1,76 @@
1
+$ ->
2
+  getFormData = (elem) ->
3
+    form_data = $("#edit_agent, #new_agent").serializeObject()
4
+    attribute = $(elem).data('attribute')
5
+    form_data['attribute'] = attribute
6
+    delete form_data['_method']
7
+    form_data
8
+
9
+  window.initializeFormCompletable = ->
10
+    returnedResults = {}
11
+    completableDefaultOptions = (input) ->
12
+      results: [
13
+        (returnedResults[$(input).data('attribute')] || {text: 'Options', children: [{id: undefined, text: 'loading ...'}]}),
14
+        {
15
+          text: 'Current',
16
+          children: [id: $(input).val(), text: $(input).val()]
17
+        },
18
+        {
19
+          text: 'Custom',
20
+          children: [id: 'manualInput', text: 'manual input']
21
+        },
22
+      ]
23
+
24
+    $("input[role~=validatable], select[role~=validatable]").on 'change', (e) =>
25
+      form_data = getFormData(e.currentTarget)
26
+      form_group = $(e.currentTarget).closest('.form-group')
27
+      $.ajax '/agents/validate',
28
+        type: 'POST',
29
+        data: form_data
30
+        success: (data) ->
31
+          form_group.addClass('has-feedback').removeClass('has-error')
32
+          form_group.find('span').addClass('hidden')
33
+          form_group.find('.glyphicon-ok').removeClass('hidden')
34
+          returnedResults = {}
35
+        error: (data) ->
36
+          form_group.addClass('has-feedback').addClass('has-error')
37
+          form_group.find('span').addClass('hidden')
38
+          form_group.find('.glyphicon-remove').removeClass('hidden')
39
+          returnedResults = {}
40
+
41
+    $("input[role~=validatable], select[role~=validatable]").trigger('change')
42
+
43
+    $.each $("input[role~=completable]"), (i, input) ->
44
+      $(input).select2(
45
+        data: ->
46
+          completableDefaultOptions(input)
47
+      ).on("change", (e) ->
48
+        if e.added && e.added.id == 'manualInput'
49
+          $(e.currentTarget).select2("destroy")
50
+          $(e.currentTarget).val(e.removed.id)
51
+      )
52
+
53
+    updateDropdownData = (form_data, element, data) ->
54
+      returnedResults[form_data.attribute] = {text: 'Options', children: data}
55
+      $(element).trigger('change')
56
+      $(element).select2('open')
57
+
58
+    $("input[role~=completable]").on 'select2-open', (e) ->
59
+      form_data = getFormData(e.currentTarget)
60
+      return if returnedResults[form_data.attribute]
61
+
62
+      $.ajax '/agents/complete',
63
+        type: 'POST',
64
+        data: form_data
65
+        success: (data) ->
66
+          updateDropdownData(form_data, e.currentTarget, data)
67
+        error: (data) ->
68
+          updateDropdownData(form_data, e.currentTarget, [{id: undefined, text: 'Error loading data.'}])
69
+
70
+    $("input[type=radio][role~=form-configurable]").change (e) ->
71
+      input = $(e.currentTarget).parents().siblings("input[data-attribute=#{$(e.currentTarget).data('attribute')}]")
72
+      if $(e.currentTarget).val() == 'manual'
73
+        input.removeClass('hidden')
74
+      else
75
+        input.val($(e.currentTarget).val())
76
+        input.addClass('hidden')

+ 12 - 10
app/assets/javascripts/components/worker-checker.js.coffee

@@ -1,10 +1,15 @@
1 1
 $ ->
2
-  firstEventCount = null
2
+  sinceId = null
3 3
   previousJobs = null
4 4
 
5 5
   if $(".job-indicator").length
6 6
     check = ->
7
-      $.getJSON "/worker_status", (json) ->
7
+      query =
8
+        if sinceId?
9
+          '?since_id=' + sinceId
10
+        else
11
+          ''
12
+      $.getJSON "/worker_status" + query, (json) ->
8 13
         for method in ['pending', 'awaiting_retry', 'recent_failures']
9 14
           count = json[method]
10 15
           elem = $(".job-indicator[role=#{method}]")
@@ -23,16 +28,17 @@ $ ->
23 28
             if elem.is(":visible")
24 29
               elem.tooltip('destroy').fadeOut()
25 30
 
26
-        firstEventCount = json.event_count unless firstEventCount?
27
-        if firstEventCount? && json.event_count > firstEventCount
31
+        if sinceId? && json.event_count > 0
28 32
           $("#event-indicator").tooltip('destroy').
29
-                                tooltip(title: "Click to reload", delay: 0, placement: "bottom", trigger: "hover").
33
+                                tooltip(title: "Click to see the events", delay: 0, placement: "bottom", trigger: "hover").
34
+                                find('a').attr(href: json.events_url).end().
30 35
                                 fadeIn().
31 36
                                 find(".number").
32
-                                text(json.event_count - firstEventCount)
37
+                                text(json.event_count)
33 38
         else
34 39
           $("#event-indicator").tooltip('destroy').fadeOut()
35 40
 
41
+        sinceId ?= json.max_id
36 42
         currentJobs = [json.pending, json.awaiting_retry, json.recent_failures]
37 43
         if document.location.pathname == '/jobs' && $(".modal[aria-hidden=false]").length == 0 && previousJobs? && previousJobs.join(',') != currentJobs.join(',')
38 44
           $.get '/jobs', (data) =>
@@ -42,7 +48,3 @@ $ ->
42 48
         window.workerCheckTimeout = setTimeout check, 2000
43 49
 
44 50
     check()
45
-
46
-  $("#event-indicator a").on "click", (e) ->
47
-    e.preventDefault()
48
-    window.location.reload()

+ 5 - 5
app/assets/javascripts/pages/agent-edit-page.js.coffee

@@ -18,7 +18,6 @@ class @AgentEditPage
18 18
     else
19 19
       $(".agent-settings").show()
20 20
       $("#agent-spinner").fadeIn()
21
-      $("#agent_source_ids").select2("val", {})
22 21
       $(".model-errors").hide() unless firstTime
23 22
       $.getJSON "/agents/type_details", { type: type }, (json) =>
24 23
         if json.can_be_scheduled
@@ -46,11 +45,12 @@ class @AgentEditPage
46 45
 
47 46
         $(".description").show().html(json.description_html) if json.description_html?
48 47
 
49
-        $('.oauthable-form').html(json.form) if json.form?
50
-
51 48
         unless firstTime
52
-          window.jsonEditor.json = json.options
53
-          window.jsonEditor.rebuild()
49
+          $('.oauthable-form').html(json.oauthable) if json.oauthable?
50
+          $('.agent-options').html(json.form_options) if json.form_options?
51
+          window.jsonEditor = setupJsonEditor()[0]
52
+
53
+        window.initializeFormCompletable()
54 54
 
55 55
         $("#agent-spinner").stop(true, true).fadeOut();
56 56
 

+ 21 - 0
app/assets/javascripts/pages/agent-show-page.js.coffee

@@ -15,6 +15,27 @@ class @AgentShowPage
15 15
     $("#logs .refresh, #logs .clear").hide()
16 16
     $.get "/agents/#{agentId}/logs", (html) =>
17 17
       $("#logs .logs").html html
18
+      $("#logs .logs .show-log-details").each ->
19
+        $button = $(this)
20
+        $button.on 'click', (e) ->
21
+          e.preventDefault()
22
+          $("body").append """
23
+            <div class="modal fade" tabindex="-1" id='dynamic-modal' role="dialog" aria-labelledby="dynamic-modal-label" aria-hidden="true">
24
+              <div class="modal-dialog modal-lg">
25
+                <div class="modal-content">
26
+                  <div class="modal-header">
27
+                    <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
28
+                    <h4 class="modal-title" id="dynamic-modal-label"></h4>
29
+                  </div>
30
+                  <div class="modal-body"><pre></pre></div>
31
+                </div>
32
+              </div>
33
+            </div>
34
+          """
35
+          $('#dynamic-modal').find('.modal-title').text $button.data('modal-title')
36
+          $('#dynamic-modal').find('.modal-body pre').text $button.data('modal-content')
37
+          $('#dynamic-modal').modal('show').on 'hidden.bs.modal', -> $('#dynamic-modal').remove()
38
+
18 39
       $("#logs .spinner").stop(true, true).fadeOut ->
19 40
         $("#logs .refresh, #logs .clear").show()
20 41
 

+ 29 - 18
app/assets/stylesheets/application.css.scss.erb

@@ -29,15 +29,19 @@ body { padding-top: 60px; }
29 29
 
30 30
 /* Rails scaffold style compatibility */
31 31
 #error_explanation {
32
-  @extend .alert;
33
-  @extend .alert-error;
34
-  @extend .alert-block;
32
+  color: #f00;
33
+  ul {
34
+    list-style: none;
35
+    margin: 0 0 18px 0;
36
+  }
35 37
 }
36 38
 
37 39
 .field_with_errors {
38
-  @extend .control-group.error;
40
+  @extend .has-error;
39 41
 }
40 42
 
43
+.modal-body pre { white-space: pre-wrap; }
44
+
41 45
 .select2 {
42 46
   float: none !important;
43 47
   margin-left: 0 !important;
@@ -157,6 +161,7 @@ span.not-applicable:after {
157 161
   height: 16px;
158 162
   display: inline-block;
159 163
   vertical-align: inherit;
164
+  cursor: pointer;
160 165
 
161 166
   &.refresh {
162 167
     margin: 0 10px;
@@ -240,7 +245,22 @@ h2 .scenario, a span.label.scenario {
240 245
   width: 200px;
241 246
 }
242 247
 
243
-.btn-auth {
248
+$services:            twitter     37signals   github      tumblr      dropbox;
249
+$service-colors:      #55acee     #8fc857     #444444     #2c4762     #007EE5;
250
+
251
+@mixin services {
252
+  @each $service in $services {
253
+    $i: index($services, $service);
254
+    $service-color: nth($service-colors, $i);
255
+
256
+    &.service-#{$service} {
257
+      color: #fff;
258
+      background-color: $service-color;
259
+    }
260
+  }
261
+}
262
+
263
+.btn-service {
244 264
   position: relative;
245 265
   padding-left: 40px;
246 266
   $border-color: rgba(0,0,0,0.2);
@@ -259,18 +279,9 @@ h2 .scenario, a span.label.scenario {
259 279
     border-right: 1px solid $border-color;
260 280
   }
261 281
 
262
-  &.btn-auth-twitter {
263
-    color: #fff;
264
-    background-color: #55acee;
265
-  }
266
-
267
-  &.btn-auth-37signals {
268
-    color: #fff;
269
-    background-color: #8fc857;
270
-  }
282
+  @include services;
283
+}
271 284
 
272
-  &.btn-auth-github {
273
-    color: #fff;
274
-    background-color: #444;
275
-  }
285
+.label-service {
286
+  @include services;
276 287
 }

+ 14 - 0
app/assets/stylesheets/tables.css.scss

@@ -20,6 +20,20 @@
20 20
   }
21 21
 }
22 22
 
23
+.table-striped > tbody > tr.hl {
24
+  &:nth-child(odd) {
25
+    > td, > th {
26
+      background-color: #ffeecc;
27
+    }
28
+  }
29
+
30
+  &:nth-child(even) {
31
+    > td, > th {
32
+      background-color: #f9e8c6;
33
+    }
34
+  }
35
+}
36
+
23 37
 table.events {
24 38
   .payload {
25 39
     color: #999;

+ 34 - 9
app/concerns/agent_controller_concern.rb

@@ -12,11 +12,11 @@ module AgentControllerConcern
12 12
   end
13 13
 
14 14
   def control_action
15
-    options['action'].presence || 'run'
15
+    interpolated['action']
16 16
   end
17 17
 
18 18
   def validate_control_action
19
-    case control_action
19
+    case options['action']
20 20
     when 'run'
21 21
       control_targets.each { |target|
22 22
         if target.cannot_be_scheduled?
@@ -24,24 +24,49 @@ module AgentControllerConcern
24 24
         end
25 25
       }
26 26
     when 'enable', 'disable'
27
+    when nil
28
+      errors.add(:base, "action must be specified")
29
+    when /\{[%{]/
30
+      # Liquid template
27 31
     else
28 32
       errors.add(:base, 'invalid action')
29 33
     end
30 34
   end
31 35
 
32 36
   def control!
33
-    control_targets.active.each { |target|
37
+    control_targets.each { |target|
34 38
       begin
35 39
         case control_action
36 40
         when 'run'
37
-          log "Agent run queued for '#{target.name}'"
38
-          Agent.async_check(target.id)
41
+          case
42
+          when target.cannot_be_scheduled?
43
+            error "'#{target.name}' cannot run without an incoming event"
44
+          when target.disabled?
45
+            log "Agent run ignored for disabled Agent '#{target.name}'"
46
+          else
47
+            Agent.async_check(target.id)
48
+            log "Agent run queued for '#{target.name}'"
49
+          end
39 50
         when 'enable'
40
-          log "Enabling the Agent '#{target.name}'"
41
-          target.update!(disable: false) if target.disabled?
51
+          case
52
+          when target.disabled?
53
+            target.update!(disabled: false)
54
+            log "Agent '#{target.name}' is enabled"
55
+          else
56
+            log "Agent '#{target.name}' is already enabled"
57
+          end
42 58
         when 'disable'
43
-          log "Disabling the Agent '#{target.name}'"
44
-          target.update!(disable: true) unless target.disabled?
59
+          case
60
+          when target.disabled?
61
+            log "Agent '#{target.name}' is alread disabled"
62
+          else
63
+            target.update!(disabled: true)
64
+            log "Agent '#{target.name}' is disabled"
65
+          end
66
+        when ''
67
+          # Do nothing
68
+        else
69
+          error "Unsupported action '#{control_action}' ignored for '#{target.name}'"
45 70
         end
46 71
       rescue => e
47 72
         error "Failed to #{control_action} '#{target.name}': #{e.message}"

+ 35 - 0
app/concerns/dropbox_concern.rb

@@ -0,0 +1,35 @@
1
+module DropboxConcern
2
+  extend ActiveSupport::Concern
3
+
4
+  included do
5
+    include Oauthable
6
+    valid_oauth_providers :dropbox
7
+    gem_dependency_check { defined?(Dropbox) && Devise.omniauth_providers.include?(:dropbox) }
8
+  end
9
+
10
+  def dropbox
11
+    Dropbox::API::Config.app_key = consumer_key
12
+    Dropbox::API::Config.app_secret = consumer_secret
13
+    Dropbox::API::Config.mode = 'dropbox'
14
+    Dropbox::API::Client.new(token: oauth_token, secret: oauth_token_secret)
15
+  end
16
+
17
+  private
18
+
19
+  def consumer_key
20
+    (config = Devise.omniauth_configs[:dropbox]) && config.strategy.consumer_key
21
+  end
22
+
23
+  def consumer_secret
24
+    (config = Devise.omniauth_configs[:dropbox]) && config.strategy.consumer_secret
25
+  end
26
+
27
+  def oauth_token
28
+    service && service.token
29
+  end
30
+
31
+  def oauth_token_secret
32
+    service && service.secret
33
+  end
34
+
35
+end

+ 66 - 0
app/concerns/form_configurable.rb

@@ -0,0 +1,66 @@
1
+module FormConfigurable
2
+  extend ActiveSupport::Concern
3
+
4
+  included do
5
+    class_attribute :_form_configurable_fields
6
+    self._form_configurable_fields = HashWithIndifferentAccess.new { |h,k| h[k] = [] }
7
+  end
8
+
9
+  delegate :form_configurable_attributes, to: :class
10
+  delegate :form_configurable_fields, to: :class
11
+
12
+  def is_form_configurable?
13
+    true
14
+  end
15
+
16
+  def validate_option(method)
17
+    if self.respond_to? "validate_#{method}".to_sym
18
+      self.send("validate_#{method}".to_sym)
19
+    else
20
+      false
21
+    end
22
+  end
23
+
24
+  def complete_option(method)
25
+    if self.respond_to? "complete_#{method}".to_sym
26
+      self.send("complete_#{method}".to_sym)
27
+    end
28
+  end
29
+
30
+  module ClassMethods
31
+    def form_configurable(name, *args)
32
+      options = args.extract_options!.reverse_merge(roles: [], type: :string)
33
+
34
+      if args.all? { |arg| arg.is_a?(Symbol) }
35
+        options.assert_valid_keys([:type, :roles, :values])
36
+      end
37
+
38
+      if options[:type] == :array && (options[:values].blank? || !options[:values].is_a?(Array))
39
+        raise ArgumentError.new('When using :array as :type you need to provide the :values as an Array')
40
+      end
41
+
42
+      if options[:roles].is_a?(Symbol)
43
+        options[:roles] = [options[:roles]]
44
+      end
45
+
46
+      if options[:type] == :array
47
+        options[:roles] << :completable
48
+        class_eval <<-EOF
49
+          def complete_#{name}
50
+            #{options[:values]}.map { |v| {text: v, id: v} }
51
+          end
52
+        EOF
53
+      end
54
+
55
+      _form_configurable_fields[name] = options
56
+    end
57
+
58
+    def form_configurable_fields
59
+      self._form_configurable_fields
60
+    end
61
+
62
+    def form_configurable_attributes
63
+      form_configurable_fields.keys
64
+    end
65
+  end
66
+end

+ 3 - 3
app/concerns/twitter_concern.rb

@@ -7,7 +7,7 @@ module TwitterConcern
7 7
     validate :validate_twitter_options
8 8
     valid_oauth_providers :twitter
9 9
 
10
-    gem_dependency_check { defined?(Twitter) && Devise.omniauth_providers.include?(:twitter) }
10
+    gem_dependency_check { defined?(Twitter) && Devise.omniauth_providers.include?(:twitter) && ENV['TWITTER_OAUTH_KEY'].present? && ENV['TWITTER_OAUTH_SECRET'].present? }
11 11
   end
12 12
 
13 13
   def validate_twitter_options
@@ -46,9 +46,9 @@ module TwitterConcern
46 46
 
47 47
   module ClassMethods
48 48
     def twitter_dependencies_missing
49
-      if defined?(Twitter)
49
+      if ENV['TWITTER_OAUTH_KEY'].blank? || ENV['TWITTER_OAUTH_SECRET'].blank?
50 50
         "## Set TWITTER_OAUTH_KEY and TWITTER_OAUTH_SECRET in your environment to use Twitter Agents."
51
-      else
51
+      elsif !defined?(Twitter) || !Devise.omniauth_providers.include?(:twitter)
52 52
         "## Include the `twitter`, `omniauth-twitter`, and `cantino-twitter-stream` gems in your Gemfile to use Twitter Agents."
53 53
       end
54 54
     end

+ 38 - 4
app/controllers/agents_controller.rb

@@ -35,6 +35,8 @@ class AgentsController < ApplicationController
35 35
 
36 36
   def type_details
37 37
     @agent = Agent.build_for_type(params[:type], current_user, {})
38
+    initialize_presenter
39
+
38 40
     render :json => {
39 41
         :can_be_scheduled => @agent.can_be_scheduled?,
40 42
         :default_schedule => @agent.default_schedule,
@@ -43,7 +45,8 @@ class AgentsController < ApplicationController
43 45
         :can_control_other_agents => @agent.can_control_other_agents?,
44 46
         :options => @agent.default_options,
45 47
         :description_html => @agent.html_description,
46
-        :form => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent })
48
+        :oauthable => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }),
49
+        :form_options => render_to_string(partial: 'options', locals: { agent: @agent })
47 50
     }
48 51
   end
49 52
 
@@ -92,6 +95,7 @@ class AgentsController < ApplicationController
92 95
     else
93 96
       @agent = agents.build
94 97
     end
98
+    initialize_presenter
95 99
 
96 100
     respond_to do |format|
97 101
       format.html
@@ -101,17 +105,18 @@ class AgentsController < ApplicationController
101 105
 
102 106
   def edit
103 107
     @agent = current_user.agents.find(params[:id])
108
+    initialize_presenter
104 109
   end
105 110
 
106 111
   def create
107
-    @agent = Agent.build_for_type(params[:agent].delete(:type),
108
-                                  current_user,
109
-                                  params[:agent])
112
+    build_agent
113
+
110 114
     respond_to do |format|
111 115
       if @agent.save
112 116
         format.html { redirect_back "'#{@agent.name}' was successfully created." }
113 117
         format.json { render json: @agent, status: :ok, location: agent_path(@agent) }
114 118
       else
119
+        initialize_presenter
115 120
         format.html { render action: "new" }
116 121
         format.json { render json: @agent.errors, status: :unprocessable_entity }
117 122
       end
@@ -126,6 +131,7 @@ class AgentsController < ApplicationController
126 131
         format.html { redirect_back "'#{@agent.name}' was successfully updated." }
127 132
         format.json { render json: @agent, status: :ok, location: agent_path(@agent) }
128 133
       else
134
+        initialize_presenter
129 135
         format.html { render action: "edit" }
130 136
         format.json { render json: @agent.errors, status: :unprocessable_entity }
131 137
       end
@@ -153,6 +159,22 @@ class AgentsController < ApplicationController
153 159
     end
154 160
   end
155 161
 
162
+  def validate
163
+    build_agent
164
+
165
+    if @agent.validate_option(params[:attribute])
166
+      render text: 'ok'
167
+    else
168
+      render text: 'error', status: 403
169
+    end
170
+  end
171
+
172
+  def complete
173
+    build_agent
174
+
175
+    render json: @agent.complete_option(params[:attribute])
176
+  end
177
+
156 178
   protected
157 179
 
158 180
   # Sanitize params[:return] to prevent open redirect attacks, a common security issue.
@@ -167,4 +189,16 @@ class AgentsController < ApplicationController
167 189
 
168 190
     redirect_to path, notice: message
169 191
   end
192
+
193
+  def build_agent
194
+    @agent = Agent.build_for_type(params[:agent].delete(:type),
195
+                                  current_user,
196
+                                  params[:agent])
197
+  end
198
+
199
+  def initialize_presenter
200
+    if @agent.present? && @agent.is_form_configurable?
201
+      @agent = FormConfigurableAgentPresenter.new(@agent, view_context)
202
+    end
203
+  end
170 204
 end

+ 1 - 1
app/controllers/application_controller.rb

@@ -1,7 +1,7 @@
1 1
 class ApplicationController < ActionController::Base
2 2
   protect_from_forgery
3 3
 
4
-  before_filter :authenticate_user!
4
+  before_action :authenticate_user!
5 5
   before_action :configure_permitted_parameters, if: :devise_controller?
6 6
 
7 7
   helper :all

+ 1 - 1
app/controllers/events_controller.rb

@@ -1,5 +1,5 @@
1 1
 class EventsController < ApplicationController
2
-  before_filter :load_event, :except => :index
2
+  before_action :load_event, except: :index
3 3
 
4 4
   def index
5 5
     if params[:agent_id]

+ 2 - 2
app/controllers/home_controller.rb

@@ -1,7 +1,7 @@
1 1
 class HomeController < ApplicationController
2
-  skip_before_filter :authenticate_user!
2
+  skip_before_action :authenticate_user!
3 3
 
4
-  before_filter :upgrade_warning, only: :index
4
+  before_action :upgrade_warning, only: :index
5 5
 
6 6
   def index
7 7
   end

+ 1 - 1
app/controllers/jobs_controller.rb

@@ -1,5 +1,5 @@
1 1
 class JobsController < ApplicationController
2
-  before_filter :authenticate_admin!
2
+  before_action :authenticate_admin!
3 3
 
4 4
   def index
5 5
     @jobs = Delayed::Job.order("coalesce(failed_at,'1000-01-01'), run_at asc").page(params[:page])

+ 1 - 1
app/controllers/logs_controller.rb

@@ -1,5 +1,5 @@
1 1
 class LogsController < ApplicationController
2
-  before_filter :load_agent
2
+  before_action :load_agent
3 3
 
4 4
   def index
5 5
     @logs = @agent.logs.all

+ 15 - 0
app/controllers/omniauth_callbacks_controller.rb

@@ -0,0 +1,15 @@
1
+class OmniauthCallbacksController < Devise::OmniauthCallbacksController
2
+  def action_missing(name)
3
+    case name.to_sym
4
+    when *Devise.omniauth_providers
5
+      service = current_user.services.initialize_or_update_via_omniauth(request.env['omniauth.auth'])
6
+      if service && service.save
7
+        redirect_to services_path, notice: "The service was successfully created."
8
+      else
9
+        redirect_to services_path, error: "Error creating the service."
10
+      end
11
+    else
12
+      raise ActionController::RoutingError, 'not found'
13
+    end
14
+  end
15
+end

+ 1 - 1
app/controllers/scenarios_controller.rb

@@ -1,6 +1,6 @@
1 1
 class ScenariosController < ApplicationController
2 2
   include SortableTable
3
-  skip_before_filter :authenticate_user!, :only => :export
3
+  skip_before_action :authenticate_user!, only: :export
4 4
 
5 5
   def index
6 6
     set_table_sort sorts: %w[name public], default: { name: :asc }

+ 1 - 10
app/controllers/services_controller.rb

@@ -1,7 +1,7 @@
1 1
 class ServicesController < ApplicationController
2 2
   include SortableTable
3 3
 
4
-  before_filter :upgrade_warning, only: :index
4
+  before_action :upgrade_warning, only: :index
5 5
 
6 6
   def index
7 7
     set_table_sort sorts: %w[provider name global], default: { provider: :asc }
@@ -33,13 +33,4 @@ class ServicesController < ApplicationController
33 33
       format.json { render json: @service }
34 34
     end
35 35
   end
36
-
37
-  def callback
38
-    @service = current_user.services.initialize_or_update_via_omniauth(request.env['omniauth.auth'])
39
-    if @service && @service.save
40
-      redirect_to services_path, notice: "The service was successfully created."
41
-    else
42
-      redirect_to services_path, error: "Error creating the service."
43
-    end
44
-  end
45 36
 end

+ 2 - 1
app/controllers/web_requests_controller.rb

@@ -16,7 +16,8 @@
16 16
 #   ["not found", 404, 'text/plain']
17 17
 
18 18
 class WebRequestsController < ApplicationController
19
-  skip_before_filter :authenticate_user!
19
+  skip_before_action :verify_authenticity_token
20
+  skip_before_action :authenticate_user!
20 21
 
21 22
   def handle_request
22 23
     user = User.find_by_id(params[:user_id])

+ 27 - 7
app/controllers/worker_status_controller.rb

@@ -1,12 +1,32 @@
1 1
 class WorkerStatusController < ApplicationController
2 2
   def show
3
-    start = Time.now.to_f
4
-    render :json => {
5
-        :pending => Delayed::Job.where("run_at <= ? AND locked_at IS NULL AND attempts = 0", Time.now).count,
6
-        :awaiting_retry => Delayed::Job.where("failed_at IS NULL AND attempts > 0").count,
7
-        :recent_failures => Delayed::Job.where("failed_at IS NOT NULL AND failed_at > ?", 5.days.ago).count,
8
-        :event_count => current_user.events.count,
9
-        :compute_time => Time.now.to_f - start
3
+    start = Time.now
4
+    events = current_user.events
5
+
6
+    if params[:since_id].present?
7
+      since_id = params[:since_id].to_i
8
+      events = events.where('id > ?', since_id)
9
+    end
10
+
11
+    result = events.select('COUNT(id) AS count', 'MIN(id) AS min_id', 'MAX(id) AS max_id').reorder('min(created_at)').first
12
+    count, min_id, max_id = result.count, result.min_id, result.max_id
13
+
14
+    case max_id
15
+    when nil
16
+    when min_id
17
+      events_url = events_path(hl: max_id)
18
+    else
19
+      events_url = events_path(hl: "#{min_id}-#{max_id}")
20
+    end
21
+
22
+    render json: {
23
+      pending: Delayed::Job.pending.where("run_at <= ?", start).count,
24
+      awaiting_retry: Delayed::Job.awaiting_retry.count,
25
+      recent_failures: Delayed::Job.failed.where('failed_at > ?', 5.days.ago).count,
26
+      event_count: count,
27
+      max_id: max_id || 0,
28
+      events_url: events_url,
29
+      compute_time: Time.now - start
10 30
     }
11 31
   end
12 32
 end

+ 1 - 1
app/helpers/agent_helper.rb

@@ -1,7 +1,7 @@
1 1
 module AgentHelper
2 2
   def agent_show_view(agent)
3 3
     name = agent.short_type.underscore
4
-    if File.exists?(Rails.root.join("app", "views", "agents", "agent_views", name, "_show.html.erb"))
4
+    if File.exist?(Rails.root.join("app", "views", "agents", "agent_views", name, "_show.html.erb"))
5 5
       File.join("agents", "agent_views", name, "show")
6 6
     end
7 7
   end

+ 63 - 8
app/helpers/application_helper.rb

@@ -1,8 +1,20 @@
1 1
 module ApplicationHelper
2
-  def nav_link(name, path, options = {}, &block)
3
-    if glyphicon = options.delete(:glyphicon)
4
-      name = "<span class='glyphicon glyphicon-#{glyphicon}'></span> ".html_safe + name
2
+  def icon_tag(name, options = {})
3
+    if dom_class = options[:class]
4
+      dom_class = ' ' << dom_class
5 5
     end
6
+
7
+    case name
8
+    when /\Aglyphicon-/
9
+      "<span class='glyphicon #{name}#{dom_class}'></span>".html_safe
10
+    when /\Afa-/
11
+      "<i class='fa #{name}#{dom_class}'></i>".html_safe
12
+    else
13
+      raise "Unrecognized icon name: #{name}"
14
+    end
15
+  end
16
+
17
+  def nav_link(name, path, options = {}, &block)
6 18
     content = link_to(name, path, options)
7 19
     active = current_page?(path)
8 20
     if block
@@ -41,12 +53,55 @@ module ApplicationHelper
41 53
     end
42 54
   end
43 55
 
44
-  def icon_for_service(service)
45
-    case service.to_sym
46
-    when :twitter, :tumblr, :github
47
-      "<i class='fa fa-#{service}'></i>".html_safe
56
+  def omniauth_provider_icon(provider)
57
+    case provider.to_sym
58
+    when :twitter, :tumblr, :github, :dropbox
59
+      icon_tag("fa-#{provider}")
48 60
     else
49
-      "<i class='fa fa-lock'></i>".html_safe
61
+      icon_tag("fa-lock")
50 62
     end
51 63
   end
64
+
65
+  def omniauth_provider_name(provider)
66
+    t("devise.omniauth_providers.#{provider}")
67
+  end
68
+
69
+  def omniauth_button(provider)
70
+    link_to [
71
+      omniauth_provider_icon(provider),
72
+      content_tag(:span, "Authenticate with #{omniauth_provider_name(provider)}")
73
+    ].join.html_safe, user_omniauth_authorize_path(provider), class: "btn btn-default btn-service service-#{provider}"
74
+  end
75
+
76
+  def service_label_text(service)
77
+    "#{omniauth_provider_name(service.provider)} - #{service.name}"
78
+  end
79
+
80
+  def service_label(service)
81
+    content_tag :span, [
82
+      omniauth_provider_icon(service.provider),
83
+      service_label_text(service)
84
+    ].join.html_safe, class: "label label-default label-service service-#{service.provider}"
85
+  end
86
+
87
+  def highlighted?(id)
88
+    @highlighted_ranges ||=
89
+      case value = params[:hl].presence
90
+      when String
91
+        value.split(/,/).flat_map { |part|
92
+          case part
93
+          when /\A(\d+)\z/
94
+            (part.to_i)..(part.to_i)
95
+          when /\A(\d+)?-(\d+)?\z/
96
+            ($1 ? $1.to_i : 1)..($2 ? $2.to_i : Float::INFINITY)
97
+          else
98
+            []
99
+          end
100
+        }
101
+      else
102
+        []
103
+      end
104
+
105
+    @highlighted_ranges.any? { |range| range.cover?(id) }
106
+  end
52 107
 end

+ 6 - 2
app/models/agent.rb

@@ -88,6 +88,10 @@ class Agent < ActiveRecord::Base
88 88
     # Implement me in your subclass of Agent.
89 89
   end
90 90
 
91
+  def is_form_configurable?
92
+    false
93
+  end
94
+
91 95
   def receive_web_request(params, method, format)
92 96
     # Implement me in your subclass of Agent.
93 97
     ["not implemented", 404]
@@ -384,7 +388,7 @@ class Agent < ActiveRecord::Base
384 388
         agent.last_receive_at = Time.now
385 389
         agent.save!
386 390
       rescue => e
387
-        agent.error "Exception during receive: #{e.message} -- #{e.backtrace}"
391
+        agent.error "Exception during receive. #{e.message}: #{e.backtrace.join("\n")}"
388 392
         raise
389 393
       end
390 394
     end
@@ -422,7 +426,7 @@ class Agent < ActiveRecord::Base
422 426
         agent.last_check_at = Time.now
423 427
         agent.save!
424 428
       rescue => e
425
-        agent.error "Exception during check: #{e.message} -- #{e.backtrace}"
429
+        agent.error "Exception during check. #{e.message}: #{e.backtrace.join("\n")}"
426 430
         raise
427 431
       end
428 432
     end

+ 1 - 1
app/models/agent_log.rb

@@ -32,6 +32,6 @@ class AgentLog < ActiveRecord::Base
32 32
   protected
33 33
 
34 34
   def truncate_message
35
-    self.message = message[0...2048] if message.present?
35
+    self.message = message[0...10_000] if message.present?
36 36
   end
37 37
 end

+ 23 - 12
app/models/agents/basecamp_agent.rb

@@ -1,21 +1,16 @@
1 1
 module Agents
2 2
   class BasecampAgent < Agent
3
-    cannot_receive_events!
4
-
3
+    include FormConfigurable
5 4
     include Oauthable
6 5
     valid_oauth_providers :'37signals'
7 6
 
7
+    cannot_receive_events!
8
+
8 9
     description <<-MD
9 10
       The BasecampAgent checks a Basecamp project for new Events
10 11
 
11 12
       To be able to use this Agent you need to authenticate with 37signals in the [Services](/services) section first.
12 13
 
13
-      You need to provide the `project_id` of the project you want to monitor.
14
-      If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows:
15
-
16
-      `https://basecamp.com/123456/projects/`
17
-      project_id
18
-      `-explore-basecamp`
19 14
     MD
20 15
 
21 16
     event_description <<-MD
@@ -50,6 +45,14 @@ module Agents
50 45
       }
51 46
     end
52 47
 
48
+    form_configurable :project_id, roles: :completable
49
+
50
+    def complete_project_id
51
+      service.prepare_request
52
+      response = HTTParty.get projects_url, request_options.merge(query_parameters)
53
+      response.map { |p| {text: "#{p['name']} (#{p['id']})", id: p['id']}}
54
+    end
55
+
53 56
     def validate_options
54 57
       errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present?
55 58
     end
@@ -60,8 +63,8 @@ module Agents
60 63
 
61 64
     def check
62 65
       service.prepare_request
63
-      reponse = HTTParty.get request_url, request_options.merge(query_parameters)
64
-      events = JSON.parse(reponse.body)
66
+      response = HTTParty.get events_url, request_options.merge(query_parameters)
67
+      events = JSON.parse(response.body)
65 68
       if !memory[:last_event].nil?
66 69
         events.each do |event|
67 70
           create_event :payload => event
@@ -72,8 +75,16 @@ module Agents
72 75
     end
73 76
 
74 77
   private
75
-    def request_url
76
-      "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json"
78
+    def base_url
79
+      "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/"
80
+    end
81
+
82
+    def events_url
83
+      base_url + "projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json"
84
+    end
85
+
86
+    def projects_url
87
+      base_url + "projects.json"
77 88
     end
78 89
 
79 90
     def request_options

+ 47 - 0
app/models/agents/commander_agent.rb

@@ -0,0 +1,47 @@
1
+module Agents
2
+  class CommanderAgent < Agent
3
+    include AgentControllerConcern
4
+
5
+    cannot_create_events!
6
+
7
+    description <<-MD
8
+      This agent is triggered by schedule or an incoming event and commands other agents ("targets") to run, disable or enable themselves.
9
+
10
+      # Action types
11
+
12
+      Set `action` to one of the action types below:
13
+
14
+      * `run`: Target Agents are run when this agent is triggered.
15
+
16
+      * `disable`: Target Agents are disabled (if not) when this agent is triggered.
17
+
18
+      * `enable`: Target Agents are enabled (if not) when this agent is triggered.
19
+
20
+      Here's a tip: you can use Liquid templating to dynamically determine the action type.  For example:
21
+
22
+      - To create a CommanderAgent that receives an event from WeatherAgent every morning to kick an agent flow that is only useful in a nice weather, try this: `{% if conditions contains 'Sunny' or conditions contains 'Cloudy' %}run{% endif %}`
23
+
24
+      - Likewise, if you have a scheduled agent flow specially crafted for rainy days, try this: `{% if conditions contains 'Rain' %}enable{% else %}disabled{% endif %}`
25
+
26
+      # Targets
27
+
28
+      Select Agents that you want to control from this CommanderAgent.
29
+    MD
30
+
31
+    def working?
32
+      true
33
+    end
34
+
35
+    def check!
36
+      control!
37
+    end
38
+
39
+    def receive(incoming_events)
40
+      incoming_events.each do |event|
41
+        interpolate_with(event) do
42
+          control!
43
+        end
44
+      end
45
+    end
46
+  end
47
+end

+ 58 - 0
app/models/agents/dropbox_file_url_agent.rb

@@ -0,0 +1,58 @@
1
+module Agents
2
+  class DropboxFileUrlAgent < Agent
3
+    include DropboxConcern
4
+
5
+    cannot_be_scheduled!
6
+
7
+    description <<-MD
8
+      #{'## Include the `dropbox-api` and `omniauth-dropbox` gems in your `Gemfile` and set `DROPBOX_OAUTH_KEY` and `DROPBOX_OAUTH_SECRET` in your environment to use Dropbox Agents.' if dependencies_missing?}
9
+      The _DropboxFileUrlAgent_ is used to work with Dropbox. It takes a file path (or multiple files paths) and emits
10
+      events with [temporary links](https://www.dropbox.com/developers/core/docs#media).
11
+
12
+      The incoming event payload needs to have a `paths` key, with a comma-separated list of files you want the URL for. For example:
13
+
14
+          {
15
+            "paths": "first/path, second/path"
16
+          }
17
+
18
+      __TIP__: You can use the _Event Formatting Agent_ to format events before they come in. Here's an example configuration for formatting an event coming out of a _Dropbox Watch Agent_:
19
+
20
+          {
21
+            "instructions": {
22
+              "paths": "{{ added | map: 'path' | join: ',' }}"
23
+            },
24
+            "matchers": [],
25
+            "mode": "clean"
26
+          }
27
+
28
+      An example of usage would be to watch a specific Dropbox directory (with the _DropboxWatchAgent_) and get the URLs for the added or updated files. You could then, for example, send emails with those links.
29
+
30
+    MD
31
+
32
+    event_description <<-MD
33
+      The event payload will contain the following fields:
34
+
35
+          {
36
+            "url": "https://dl.dropboxusercontent.com/1/view/abcdefghijk/example",
37
+            "expires": "Fri, 16 Sep 2011 01:01:25 +0000"
38
+          }
39
+    MD
40
+
41
+    def working?
42
+      !recent_error_logs?
43
+    end
44
+
45
+    def receive(events)
46
+      events.map { |e| e.payload['paths'].split(',').map(&:strip) }
47
+        .flatten.each { |path| create_event payload: url_for(path) }
48
+    end
49
+
50
+    private
51
+
52
+    def url_for(path)
53
+      dropbox.find(path).direct_url
54
+    end
55
+
56
+  end
57
+
58
+end

+ 114 - 0
app/models/agents/dropbox_watch_agent.rb

@@ -0,0 +1,114 @@
1
+module Agents
2
+  class DropboxWatchAgent < Agent
3
+    include DropboxConcern
4
+
5
+    cannot_receive_events!
6
+    default_schedule "every_1m"
7
+
8
+    description <<-MD
9
+      #{'## Include the `dropbox-api` and `omniauth-dropbox` gems in your `Gemfile` and set `DROPBOX_OAUTH_KEY` and `DROPBOX_OAUTH_SECRET` in your environment to use Dropbox Agents.' if dependencies_missing?}
10
+      The _DropboxWatchAgent_ watches the given `dir_to_watch` and emits events with the detected changes.
11
+    MD
12
+
13
+    event_description <<-MD
14
+      The event payload will contain the following fields:
15
+
16
+          {
17
+            "added": [ {
18
+              "path": "/path/to/added/file",
19
+              "rev": "1526952fd5",
20
+              "modified": "Fri, 10 Oct 2014 19:00:43 +0000"
21
+            } ],
22
+            "removed": [ ... ],
23
+            "updated": [ ... ]
24
+          }
25
+    MD
26
+
27
+    def default_options
28
+      {
29
+        'dir_to_watch' => '/',
30
+        'expected_update_period_in_days' => 1
31
+      }
32
+    end
33
+
34
+    def validate_options
35
+      errors.add(:base, 'The `dir_to_watch` property is required.') unless options['dir_to_watch'].present?
36
+      errors.add(:base, 'Invalid `expected_update_period_in_days` format.') unless options['expected_update_period_in_days'].present? && is_positive_integer?(options['expected_update_period_in_days'])
37
+    end
38
+
39
+    def working?
40
+      event_created_within?(interpolated['expected_update_period_in_days']) && !received_event_without_error?
41
+    end
42
+
43
+    def check
44
+      current_contents = ls(interpolated['dir_to_watch'])
45
+      diff = DropboxDirDiff.new(previous_contents, current_contents)
46
+      create_event(payload: diff.to_hash) unless previous_contents.nil? || diff.empty?
47
+
48
+      remember(current_contents)
49
+    end
50
+
51
+    private
52
+
53
+    def is_positive_integer?(value)
54
+      Integer(value) >= 0
55
+    rescue
56
+      false
57
+    end
58
+
59
+    def ls(dir_to_watch)
60
+      dropbox.ls(dir_to_watch).map { |entry| slice_json(entry, 'path', 'rev', 'modified') }
61
+    end
62
+
63
+    def slice_json(json, *keys)
64
+      keys.each_with_object({}){|key, hash| hash[key.to_s] = json[key.to_s]}
65
+    end
66
+
67
+    def previous_contents
68
+      self.memory['contents']
69
+    end
70
+
71
+    def remember(contents)
72
+      self.memory['contents'] = contents
73
+    end
74
+
75
+    # == Auxiliary classes ==
76
+
77
+    class DropboxDirDiff
78
+      def initialize(previous, current)
79
+        @previous, @current = [previous || [], current || []]
80
+      end
81
+
82
+      def empty?
83
+        (@previous == @current)
84
+      end
85
+
86
+      def to_hash
87
+        calculate_diff
88
+        { added: @added, removed: @removed, updated: @updated }
89
+      end
90
+
91
+      private
92
+
93
+      def calculate_diff
94
+        @updated = @current.select do |current_entry|
95
+          previous_entry = find_by_path(@previous, current_entry['path'])
96
+          (current_entry != previous_entry) && !previous_entry.nil?
97
+        end
98
+
99
+        updated_entries = @updated + @previous.select do |previous_entry|
100
+          find_by_path(@updated, previous_entry['path'])
101
+        end
102
+
103
+        @added = @current - @previous - updated_entries
104
+        @removed = @previous - @current - updated_entries
105
+      end
106
+
107
+      def find_by_path(array, path)
108
+        array.find { |entry| entry['path'] == path }
109
+      end
110
+    end
111
+
112
+  end
113
+
114
+end

+ 33 - 4
app/models/agents/hipchat_agent.rb

@@ -1,5 +1,7 @@
1 1
 module Agents
2 2
   class HipchatAgent < Agent
3
+    include FormConfigurable
4
+
3 5
     cannot_be_scheduled!
4 6
     cannot_create_events!
5 7
 
@@ -15,8 +17,10 @@ module Agents
15 17
 
16 18
       Change the `room_name` to the name of the room you want to send notifications to.
17 19
 
18
-      You can provide a `username` and a `message`. When sending a HTML formatted message change `format` to "html".
19
-      If you want your message to notify the room members change `notify` to "true".
20
+      You can provide a `username` and a `message`. If you want to use mentions change `format` to "text" ([details](https://www.hipchat.com/docs/api/method/rooms/message)).
21
+
22
+      If you want your message to notify the room members change `notify` to "True".
23
+
20 24
       Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random")
21 25
 
22 26
       Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating.
@@ -30,9 +34,29 @@ module Agents
30 34
         'message' => "Hello from Huginn!",
31 35
         'notify' => false,
32 36
         'color' => 'yellow',
37
+        'format' => 'html'
33 38
       }
34 39
     end
35 40
 
41
+    form_configurable :auth_token, roles: :validatable
42
+    form_configurable :room_name, roles: :completable
43
+    form_configurable :username
44
+    form_configurable :message, type: :text
45
+    form_configurable :notify, type: :boolean
46
+    form_configurable :color, type: :array, values: ['yellow', 'red', 'green', 'purple', 'gray', 'random']
47
+    form_configurable :format, type: :array, values: ['html', 'text']
48
+
49
+    def validate_auth_token
50
+      client.rooms
51
+      true
52
+    rescue HipChat::UnknownResponseCode
53
+      return false
54
+    end
55
+
56
+    def complete_room_name
57
+      client.rooms.collect { |room| {text: room.name, id: room.name} }
58
+    end
59
+
36 60
     def validate_options
37 61
       errors.add(:base, "you need to specify a hipchat auth_token or provide a credential named hipchat_auth_token") unless options['auth_token'].present? || credential('hipchat_auth_token').present?
38 62
       errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank?
@@ -45,12 +69,17 @@ module Agents
45 69
     def receive(incoming_events)
46 70
       incoming_events.each do |event|
47 71
         mo = interpolated(event)
48
-        client[mo[:room_name]].send(mo[:username][0..14], mo[:message], :notify => boolify(mo[:notify]), :color => mo[:color])
72
+        client[mo[:room_name]].send(mo[:username][0..14], mo[:message],
73
+                                      notify: boolify(mo[:notify]),
74
+                                      color: mo[:color],
75
+                                      message_format: mo[:format].presence || 'html'
76
+                                    )
49 77
       end
50 78
     end
51 79
 
80
+    private
52 81
     def client
53
-      @client ||= HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token'))
82
+      @client ||= HipChat::Client.new(interpolated[:auth_token].presence || credential('hipchat_auth_token'))
54 83
     end
55 84
   end
56 85
 end

+ 8 - 0
app/models/agents/imap_folder_agent.rb

@@ -453,6 +453,14 @@ module Agents
453 453
         ret
454 454
       end
455 455
 
456
+      def fetch(*args)
457
+        super || []
458
+      end
459
+
460
+      def uid_fetch(*args)
461
+        super || []
462
+      end
463
+
456 464
       def uid_fetch_mails(set)
457 465
         uid_fetch(set, 'RFC822.HEADER').map { |data|
458 466
           Message.new(self, data, folder: @folder, uidvalidity: @uidvalidity)

+ 31 - 18
app/models/agents/mqtt_agent.rb

@@ -110,31 +110,44 @@ module Agents
110 110
         incoming_events.each do |event|
111 111
           c.publish(interpolated(event)['topic'], event)
112 112
         end
113
-
114
-        c.disconnect
115 113
       end
116 114
     end
117 115
 
118 116
 
119 117
     def check
120
-      mqtt_client.connect do |c|
121
-
122
-        Timeout::timeout((interpolated['max_read_time'].presence || 15).to_i) {
123
-          c.get(interpolated['topic']) do |topic, message|
124
-
125
-            # A lot of services generate JSON. Try that first
126
-            payload = JSON.parse(message) rescue message
118
+      last_message = memory['last_message']
127 119
 
128
-            create_event :payload => { 
129
-              'topic' => topic, 
130
-              'message' => payload, 
131
-              'time' => Time.now.to_i 
132
-            }
133
-          end
134
-        } rescue TimeoutError
135
-
136
-        c.disconnect   
120
+      mqtt_client.connect do |c|
121
+        begin
122
+          Timeout.timeout((interpolated['max_read_time'].presence || 15).to_i) {
123
+            c.get_packet(interpolated['topic']) do |packet|
124
+              topic, payload = message = [packet.topic, packet.payload]
125
+
126
+              # Ignore a message if it is previously received
127
+              next if (packet.retain || packet.duplicate) && message == last_message
128
+
129
+              last_message = message
130
+
131
+              # A lot of services generate JSON, so try that.
132
+              begin
133
+                payload = JSON.parse(payload)
134
+              rescue
135
+              end
136
+
137
+              create_event payload: {
138
+                'topic' => topic,
139
+                'message' => payload,
140
+                'time' => Time.now.to_i
141
+              }
142
+            end
143
+          }
144
+        rescue Timeout::Error
145
+        end
137 146
       end
147
+
148
+      # Remember the last original (non-retain, non-duplicate) message
149
+      self.memory['last_message'] = last_message
150
+      save!
138 151
     end
139 152
 
140 153
   end

+ 12 - 10
app/models/agents/rss_agent.rb

@@ -41,6 +41,7 @@ module Agents
41 41
             "id": "829f845279611d7925146725317b868d",
42 42
             "date_published": "2014-09-11 01:30:00 -0700",
43 43
             "last_updated": "Thu, 11 Sep 2014 01:30:00 -0700",
44
+            "url": "http://example.com/...",
44 45
             "urls": [ "http://example.com/..." ],
45 46
             "description": "Some description",
46 47
             "content": "Some content",
@@ -75,16 +76,17 @@ module Agents
75 76
           entry_id = get_entry_id(entry)
76 77
           if check_and_track(entry_id)
77 78
             created_event_count += 1
78
-            create_event(:payload => {
79
-              :id => entry_id,
80
-              :date_published => entry.date_published,
81
-              :last_updated => entry.last_updated,
82
-              :urls => entry.urls,
83
-              :description => entry.description,
84
-              :content => entry.content,
85
-              :title => entry.title,
86
-              :authors => entry.authors,
87
-              :categories => entry.categories
79
+            create_event(payload: {
80
+              id: entry_id,
81
+              date_published: entry.date_published,
82
+              last_updated: entry.last_updated,
83
+              url: entry.url,
84
+              urls: entry.urls,
85
+              description: entry.description,
86
+              content: entry.content,
87
+              title: entry.title,
88
+              authors: entry.authors,
89
+              categories: entry.categories
88 90
             })
89 91
           end
90 92
         end

+ 1 - 1
app/models/agents/scheduler_agent.rb

@@ -19,7 +19,7 @@ module Agents
19 19
 
20 20
       Set `action` to one of the action types below:
21 21
 
22
-      * `run`: This is the default.  Target Agents are run at intervals.
22
+      * `run`: Target Agents are run at intervals, except for those disabled.
23 23
 
24 24
       * `disable`: Target Agents are disabled (if not) at intervals.
25 25
 

+ 2 - 1
app/models/agents/user_location_agent.rb

@@ -6,7 +6,8 @@ module Agents
6 6
 
7 7
     description do
8 8
       <<-MD
9
-        The UserLocationAgent creates events based on WebHook POSTS that contain a `latitude` and `longitude`.  You can use the POSTLocation iOS app to post your location.
9
+        The UserLocationAgent creates events based on WebHook POSTS that contain a `latitude` and `longitude`.  You can use the [POSTLocation](https://github.com/cantino/post_location) or [PostGPS](https://github.com/chriseidhof/PostGPS) iOS app to post your location.
10
+
10 11
 
11 12
         Your POST path will be `https://#{ENV['DOMAIN']}/users/#{user.id}/update_location/:secret` where `:secret` is specified in your options.
12 13
       MD

+ 1 - 1
app/models/agents/website_agent.rb

@@ -209,7 +209,7 @@ module Agents
209 209
         }
210 210
       end
211 211
     rescue => e
212
-      error e.message
212
+      error "Error when fetching url: #{e.message}\n#{e.backtrace.join("\n")}"
213 213
     end
214 214
 
215 215
     def receive(incoming_events)

+ 1 - 1
app/models/scenario_import.rb

@@ -111,7 +111,7 @@ class ScenarioImport
111 111
 
112 112
   def parse_file
113 113
     if data.blank? && file.present?
114
-      self.data = file.read
114
+      self.data = file.read.force_encoding(Encoding::UTF_8)
115 115
     end
116 116
   end
117 117
 

+ 1 - 3
app/models/service.rb

@@ -59,12 +59,10 @@ class Service < ActiveRecord::Base
59 59
 
60 60
   def self.provider_specific_options(omniauth)
61 61
     case omniauth['provider'].to_sym
62
-      when :twitter, :github
63
-        { name: omniauth['info']['nickname'] }
64 62
       when :'37signals'
65 63
         { user_id: omniauth['extra']['accounts'][0]['id'], name: omniauth['info']['name'] }
66 64
       else
67
-        { name: omniauth['info']['nickname'] }
65
+        { name: omniauth['info']['nickname'] || omniauth['info']['name'] }
68 66
     end
69 67
   end
70 68
 

+ 44 - 0
app/presenters/form_configurable_agent_presenter.rb

@@ -0,0 +1,44 @@
1
+require 'delegate'
2
+
3
+class Decorator < SimpleDelegator
4
+  def class
5
+    __getobj__.class
6
+  end
7
+end
8
+
9
+class FormConfigurableAgentPresenter < Decorator
10
+  def initialize(agent, view)
11
+    @agent = agent
12
+    @view = view
13
+    super(agent)
14
+  end
15
+
16
+  def option_field_for(attribute)
17
+    data = @agent.form_configurable_fields[attribute]
18
+    value = @agent.options[attribute.to_s] || @agent.default_options[attribute.to_s]
19
+    html_options = {role: (data[:roles] + ['form-configurable']).join(' '), data: {attribute: attribute}}
20
+
21
+    case data[:type]
22
+    when :text
23
+      @view.text_area_tag "agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3)
24
+    when :boolean
25
+      @view.content_tag 'div' do
26
+        @view.concat(@view.content_tag('label', class: 'radio-inline') do
27
+          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'true', @agent.send(:boolify, value) == true, html_options
28
+          @view.concat "True"
29
+        end)
30
+        @view.concat(@view.content_tag('label', class: 'radio-inline') do
31
+          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'false', @agent.send(:boolify, value) == false, html_options
32
+          @view.concat "False"
33
+        end)
34
+        @view.concat(@view.content_tag('label', class: 'radio-inline') do
35
+          @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'manual', @agent.send(:boolify, value) == nil, html_options
36
+          @view.concat "Manual Input"
37
+        end)
38
+        @view.concat(@view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => "form-control #{@agent.send(:boolify, value) != nil ? 'hidden' : ''}"))
39
+      end
40
+    when :array, :string
41
+      @view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => 'form-control')
42
+    end
43
+  end
44
+end

+ 9 - 9
app/views/agents/_action_menu.html.erb

@@ -1,30 +1,30 @@
1 1
 <ul class="dropdown-menu" role="menu">
2 2
   <% if agent.can_be_scheduled? %>
3 3
     <li>
4
-      <%= link_to '<span class="color-success glyphicon glyphicon-refresh"></span> Run'.html_safe, run_agent_path(agent, :return => returnTo), method: :post, :tabindex => "-1" %>
4
+      <%= link_to icon_tag('glyphicon-refresh', class: 'color-success') + ' Run', run_agent_path(agent, return: returnTo), method: :post, tabindex: "-1" %>
5 5
     </li>
6 6
   <% end %>
7 7
 
8 8
   <li>
9
-    <%= link_to '<span class="glyphicon glyphicon-eye-open"></span> Show'.html_safe, agent_path(agent) %>
9
+    <%= link_to icon_tag('glyphicon-eye-open') + ' Show'.html_safe, agent_path(agent) %>
10 10
   </li>
11 11
 
12 12
   <li class="divider"></li>
13 13
 
14 14
   <li>
15
-    <%= link_to '<span class="glyphicon glyphicon-pencil"></span> Edit agent'.html_safe, edit_agent_path(agent) %>
15
+    <%= link_to icon_tag('glyphicon-pencil') + ' Edit agent'.html_safe, edit_agent_path(agent) %>
16 16
   </li>
17 17
 
18 18
   <li>
19
-    <%= link_to '<span class="glyphicon glyphicon-plus"></span> Clone agent'.html_safe, new_agent_path(id: agent), :tabindex => "-1" %>
19
+    <%= link_to icon_tag('fa-copy') + ' Clone agent'.html_safe, new_agent_path(id: agent), tabindex: "-1" %>
20 20
   </li>
21 21
 
22 22
   <li>
23 23
     <%= link_to '#', 'data-toggle' => 'modal', 'data-target' => "#confirm-agent#{agent.id}" do %>
24 24
       <% if agent.disabled? %>
25
-        <i class="glyphicon glyphicon-play"></i> Enable agent
25
+        <%= icon_tag('glyphicon-play') %> Enable agent
26 26
       <% else %>
27
-        <i class="glyphicon glyphicon-pause"></i> Disable agent
27
+        <%= icon_tag('glyphicon-pause') %> Disable agent
28 28
       <% end %>
29 29
     <% end %>
30 30
   </li>
@@ -34,7 +34,7 @@
34 34
 
35 35
     <% agent.scenarios.each do |scenario| %>
36 36
       <li>
37
-        <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from #{scenario_label(scenario)}".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %>
37
+        <%= link_to icon_tag('glyphicon-remove-circle', class: 'color-warning') + " Remove from #{scenario_label(scenario)}".html_safe, leave_scenario_agent_path(agent, scenario_id: scenario.to_param, return: returnTo), method: :put, tabindex: "-1" %>
38 38
       </li>
39 39
     <% end %>
40 40
   <% end %>
@@ -43,12 +43,12 @@
43 43
 
44 44
   <% if agent.can_create_events? && agent.events.count > 0 %>
45 45
     <li>
46
-      <%= link_to '<span class="color-danger glyphicon glyphicon-trash"></span> Delete all events'.html_safe, remove_events_agent_path(agent, :return => returnTo), method: :delete, data: {confirm: 'Are you sure you want to delete ALL emitted events for this Agent?'}, :tabindex => "-1" %>
46
+      <%= link_to icon_tag('glyphicon-trash', class: 'color-danger') + ' Delete all events', remove_events_agent_path(agent, return: returnTo), method: :delete, data: {confirm: 'Are you sure you want to delete ALL emitted events for this Agent?'}, tabindex: "-1" %>
47 47
     </li>
48 48
   <% end %>
49 49
 
50 50
   <li>
51
-    <%= link_to '<span class="color-danger glyphicon glyphicon-remove"></span> Delete agent'.html_safe, agent_path(agent, :return => returnTo), method: :delete, data: { confirm: 'Are you sure that you want to permanently delete this Agent?' }, :tabindex => "-1" %>
51
+    <%= link_to icon_tag('glyphicon-remove', class: 'color-danger') + ' Delete agent', agent_path(agent, return: returnTo), method: :delete, data: { confirm: 'Are you sure that you want to permanently delete this Agent?' }, tabindex: "-1" %>
52 52
   </li>
53 53
 </ul>
54 54
 

+ 4 - 16
app/views/agents/_form.html.erb

@@ -16,8 +16,7 @@
16 16
     <div class="col-md-6">
17 17
       <div class="row">
18 18
 
19
-        <!-- Form controls width restricted -->
20
-        <div class="col-md-8">
19
+        <div class="col-md-12">
21 20
           <% if @agent.new_record? %>
22 21
             <div class="form-group type-select">
23 22
               <%= f.label :type %>
@@ -27,7 +26,7 @@
27 26
         </div>
28 27
 
29 28
         <div class="agent-settings">
30
-          <div class="col-md-8">
29
+          <div class="col-md-12">
31 30
             <div class="form-group">
32 31
               <%= f.label :name %>
33 32
               <%= f.text_field :name, :class => 'form-control' %>
@@ -105,19 +104,8 @@
105 104
 
106 105
           </div>
107 106
 
108
-          <!-- Form controls full width -->
109
-          <div class="col-md-12">
110
-            <div class="form-group">
111
-              <%= f.label :options %>
112
-              <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event.  It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span>
113
-              <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor">
114
-                <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %>
115
-              </textarea>
116
-            </div>
117
-
118
-            <div class="form-group">
119
-              <%= f.submit "Save", :class => "btn btn-primary" %>
120
-            </div>
107
+          <div class="col-md-12 agent-options">
108
+            <%= render partial: 'options', locals: { agent: @agent } %>
121 109
           </div>
122 110
         </div>
123 111
       </div>

+ 1 - 1
app/views/agents/_oauth_dropdown.html.erb

@@ -1,6 +1,6 @@
1 1
 <% if agent.try(:oauthable?) %>
2 2
   <div class="form-group type-select">
3 3
     <%= label_tag :service %>
4
-    <%= select_tag 'agent[service_id]', options_for_select(agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent.service_id), class: 'form-control' %>
4
+    <%= select_tag 'agent[service_id]', options_for_select(agent.valid_services_for(current_user).collect { |s| [service_label_text(s), s.id] }, agent.service_id), class: 'form-control' %>
5 5
   </div>
6 6
 <% end %>

+ 27 - 0
app/views/agents/_options.erb

@@ -0,0 +1,27 @@
1
+<% if agent.is_form_configurable? %>
2
+  <fieldset>
3
+    <% if agent.persisted? %>
4
+      <%= hidden_field_tag 'agent[type]', @agent.type %>
5
+    <% end %>
6
+    <legend>Options</legend>
7
+    <% agent.form_configurable_attributes.each do |attribute| %>
8
+      <div class="form-group">
9
+        <%= label_tag attribute %>
10
+        <%= agent.option_field_for(attribute) %>
11
+        <span class="glyphicon glyphicon-ok form-control-feedback hidden"></span>
12
+        <span class="glyphicon glyphicon-remove form-control-feedback hidden"></span>
13
+      </div>
14
+    <% end %>
15
+  </fieldset>
16
+<% else %>
17
+  <div class="form-group">
18
+    <%= label_tag :options %>
19
+    <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event.  It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span>
20
+    <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor">
21
+      <%= Utils.jsonify((agent.new_record? && agent.options == {}) ? agent.default_options : agent.options) %>
22
+    </textarea>
23
+  </div>
24
+<% end %>
25
+<div class="form-group">
26
+  <%= submit_tag "Save", :class => "btn btn-primary" %>
27
+</div>

+ 6 - 5
app/views/agents/edit.html.erb

@@ -9,17 +9,18 @@
9 9
       </div>
10 10
     </div>
11 11
   </div>
12
-
13
-  <%= render 'form' %>
12
+  <div id="agent-form">
13
+    <%= render 'form' %>
14
+  </div>
14 15
 
15 16
   <hr>
16 17
 
17 18
   <div class="row">
18 19
     <div class="col-md-12">
19 20
       <div class="btn-group">
20
-        <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %>
21
-        <%= link_to '<span class="glyphicon glyphicon-asterisk"></span> Show'.html_safe, agent_path(@agent), class: "btn btn-default" %>
21
+        <%= link_to icon_tag('glyphicon-chevron-left') + ' Back', agents_path, class: "btn btn-default" %>
22
+        <%= link_to icon_tag('glyphicon-asterisk') + ' Show', agent_path(@agent), class: "btn btn-default" %>
22 23
       </div>
23 24
     </div>
24 25
   </div>
25
-</div>
26
+</div>

+ 3 - 3
app/views/agents/index.html.erb

@@ -10,9 +10,9 @@
10 10
       <br/>
11 11
 
12 12
       <div class="btn-group">
13
-        <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Agent'.html_safe, new_agent_path, class: "btn btn-default" %>
14
-        <%= link_to '<span class="glyphicon glyphicon-refresh"></span> Run event propagation'.html_safe, propagate_agents_path, method: 'post', class: "btn btn-default" %>
15
-        <%= link_to '<span class="glyphicon glyphicon-random"></span> View diagram'.html_safe, diagram_path, class: "btn btn-default" %>
13
+        <%= link_to icon_tag('glyphicon-plus') + ' New Agent', new_agent_path, class: "btn btn-default" %>
14
+        <%= link_to icon_tag('glyphicon-refresh') + ' Run event propagation', propagate_agents_path, method: 'post', class: "btn btn-default" %>
15
+        <%= link_to icon_tag('glyphicon-random') + ' View diagram', diagram_path, class: "btn btn-default" %>
16 16
       </div>
17 17
     </div>
18 18
   </div>

+ 5 - 3
app/views/agents/new.html.erb

@@ -8,15 +8,17 @@
8 8
         </h2>
9 9
       </div>
10 10
 
11
-      <%= render 'form' %>
11
+      <div id="agent-form">
12
+        <%= render 'form' %>
13
+      </div>
12 14
 
13 15
       <hr>
14 16
 
15 17
       <div class="row">
16 18
         <div class="col-md-12">
17
-          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %>
19
+          <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, agents_path, class: "btn btn-default" %>
18 20
         </div>
19 21
       </div>
20 22
     </div>
21 23
   </div>
22
-</div>
24
+</div>

+ 9 - 2
app/views/agents/show.html.erb

@@ -2,7 +2,7 @@
2 2
   <div class='row'>
3 3
     <div class='col-md-2'>
4 4
         <ul class="nav nav-pills nav-stacked" id="show-tabs">
5
-          <li><%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path %></li>
5
+          <li><%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, agents_path %></li>
6 6
 
7 7
           <% if agent_show_view(@agent).present? %>
8 8
             <li class='active'><a href="#summary" data-toggle="tab"><span class='glyphicon glyphicon-picture'></span> Summary</a></li>
@@ -15,7 +15,7 @@
15 15
           <li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>" class='<%= @agent.recent_error_logs? ? 'recent-errors' : '' %>'><span class='glyphicon glyphicon-list-alt'></span> Logs</a></li>
16 16
 
17 17
           <% if @agent.can_create_events? && @agent.events.count > 0 %>
18
-            <li><%= link_to '<span class="glyphicon glyphicon-random"></span> Events'.html_safe, agent_events_path(@agent) %></li>
18
+            <li><%= link_to icon_tag('glyphicon-random') + ' Events'.html_safe, agent_events_path(@agent) %></li>
19 19
           <% else %>
20 20
             <li class='disabled'><a><span class='glyphicon glyphicon-random'></span> Events</a></li>
21 21
           <% end %>
@@ -107,6 +107,13 @@
107 107
               </p>
108 108
             <% end %>
109 109
 
110
+            <% if @agent.try(:oauthable?) %>
111
+              <p>
112
+                <b>Service:</b>
113
+                <%= service_label(@agent.service) %>
114
+              </p>
115
+            <% end %>
116
+
110 117
             <% if @agent.can_receive_events? %>
111 118
               <p>
112 119
                 <b>Event sources:</b>

+ 4 - 4
app/views/devise/confirmations/new.html.erb

@@ -4,18 +4,18 @@
4 4
 
5 5
       <h2>Resend confirmation instructions</h2>
6 6
 
7
-      <%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %>
7
+      <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'form-horizontal' }) do |f| %>
8 8
         <%= devise_error_messages! %>
9 9
 
10 10
         <div class="form-group">
11 11
           <%= f.label :email %>
12
-          <%= f.email_field :email, :class => 'form-control' %>
12
+          <%= f.email_field :email, autofocus: true, class: 'form-control' %>
13 13
         </div>
14 14
 
15
-        <%= f.submit "Resend confirmation instructions", :class => "btn btn-primary" %>
15
+        <%= f.submit "Resend confirmation instructions", class: "btn btn-primary" %>
16 16
       <% end %>
17 17
 
18 18
       <%= render "devise/shared/links" %>
19 19
     </div>
20 20
   </div>
21
-</div>
21
+</div>

+ 2 - 2
app/views/devise/mailer/confirmation_instructions.html.erb

@@ -1,5 +1,5 @@
1
-<p>Welcome <%= @resource.email %>!</p>
1
+<p>Welcome <%= @email %>!</p>
2 2
 
3 3
 <p>You can confirm your account email through the link below:</p>
4 4
 
5
-<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %></p>
5
+<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>

+ 2 - 2
app/views/devise/mailer/reset_password_instructions.html.erb

@@ -1,8 +1,8 @@
1 1
 <p>Hello <%= @resource.email %>!</p>
2 2
 
3
-<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
3
+<p>Someone has requested a link to change your password. You can do this through the link below.</p>
4 4
 
5
-<p><%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %></p>
5
+<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
6 6
 
7 7
 <p>If you didn't request this, please ignore this email.</p>
8 8
 <p>Your password won't change until you access the link above and create a new one.</p>

+ 2 - 2
app/views/devise/mailer/unlock_instructions.html.erb

@@ -1,7 +1,7 @@
1 1
 <p>Hello <%= @resource.email %>!</p>
2 2
 
3
-<p>Your account has been locked due to an excessive amount of unsuccessful sign in attempts.</p>
3
+<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
4 4
 
5 5
 <p>Click the link below to unlock your account:</p>
6 6
 
7
-<p><%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %></p>
7
+<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>

+ 7 - 7
app/views/devise/passwords/edit.html.erb

@@ -4,26 +4,26 @@
4 4
       <div class='well'>
5 5
         <h2>Change your password</h2>
6 6
 
7
-        <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, :class => 'form-horizontal' }) do |f| %>
7
+        <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| %>
8 8
           <%= devise_error_messages! %>
9 9
           <%= f.hidden_field :reset_password_token %>
10 10
 
11 11
           <div class="control-group">
12
-            <%= f.label :password, "New password", :class => 'control-label' %>
12
+            <%= f.label :password, "New password", class: 'control-label' %>
13 13
             <div class="controls">
14
-              <%= f.password_field :password, :class => 'span4' %>
14
+              <%= f.password_field :password, autofocus: true, autocomplete: "off", class: 'span4' %>
15 15
             </div>
16 16
           </div>
17 17
 
18 18
           <div class="control-group">
19
-            <%= f.label :password_confirmation, "Confirm new password", :class => 'control-label' %>
19
+            <%= f.label :password_confirmation, "Confirm new password" %>
20 20
             <div class="controls">
21
-              <%= f.password_field :password_confirmation, :class => 'span4' %>
21
+              <%= f.password_field :password_confirmation, autocomplete: "off", class: 'span4' %>
22 22
             </div>
23 23
           </div>
24 24
 
25 25
           <div class='form-actions'>
26
-            <%= f.submit "Change my password", :class => "btn btn-primary" %>
26
+            <%= f.submit "Change my password", class: "btn btn-primary" %>
27 27
           </div>
28 28
         <% end %>
29 29
 
@@ -31,4 +31,4 @@
31 31
       </div>
32 32
     </div>
33 33
   </div>
34
-</div>
34
+</div>

+ 4 - 4
app/views/devise/passwords/new.html.erb

@@ -3,19 +3,19 @@
3 3
     <div class='well'>
4 4
       <h2>Forgot your password?</h2>
5 5
 
6
-      <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %>
6
+      <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'form-horizontal' }) do |f| %>
7 7
         <%= devise_error_messages! %>
8 8
 
9 9
         <div class="form-group">
10 10
           <%= f.label :login, :class => 'col-md-2 col-md-offset-2 control-label' %>
11 11
           <div class="col-md-6">
12
-            <%= f.text_field :login, :class => 'form-control' %>
12
+            <%= f.text_field :login, autofocus: true, :class => 'form-control' %>
13 13
           </div>
14 14
         </div>
15 15
 
16 16
         <div class="form-group">
17 17
           <div class="col-md-offset-4 col-md-10">
18
-            <%= f.submit "Send me reset password instructions", :class => "btn btn-primary" %>
18
+            <%= f.submit "Send me reset password instructions", class: "btn btn-primary" %>
19 19
           </div>
20 20
         </div>
21 21
       <% end %>
@@ -25,4 +25,4 @@
25 25
       <%= render "devise/shared/links" %>
26 26
     </div>
27 27
   </div>
28
-</div>
28
+</div>

+ 22 - 18
app/views/devise/registrations/edit.html.erb

@@ -5,66 +5,70 @@
5 5
 
6 6
         <h2>Edit <%= resource_name.to_s.humanize %></h2>
7 7
 
8
-        <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put, :class => 'form-horizontal' }) do |f| %>
8
+        <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| %>
9 9
           <%= devise_error_messages! %>
10 10
 
11 11
           <div class="form-group">
12
-            <%= f.label :email, :class => 'col-md-4 control-label' %>
12
+            <%= f.label :email, class: 'col-md-4 control-label' %>
13 13
             <div class="col-md-6">
14
-              <%= f.email_field :email, :class => 'form-control' %>
14
+              <%= f.email_field :email, autofocus: true, class: 'form-control' %>
15 15
             </div>
16 16
           </div>
17 17
 
18
+          <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
19
+            <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
20
+          <% end %>
21
+
18 22
           <div class="form-group">
19
-            <%= f.label :username, :class => 'col-md-4 control-label' %>
23
+            <%= f.label :username, class: 'col-md-4 control-label' %>
20 24
             <div class="col-md-6">
21
-              <%= f.text_field :username, :class => 'form-control' %>
25
+              <%= f.text_field :username, class: 'form-control' %>
22 26
             </div>
23 27
           </div>
24 28
 
25 29
           <div class="form-group">
26
-            <%= f.label :current_password, :class => 'col-md-4 control-label' %>
30
+            <%= f.label :current_password, class: 'col-md-4 control-label' %>
27 31
             <div class="col-md-6">
28
-              <%= f.password_field :current_password, :class => 'form-control' %>
32
+              <%= f.password_field :current_password, class: 'form-control' %>
29 33
               <span class='help-inline'>We need your current password to confirm your changes.</span>
30 34
             </div>
31 35
           </div>
32
-          
36
+
33 37
           <div class="form-group">
34 38
             <div class="col-md-offset-4 col-md-10">
35
-              <%= f.submit "Update", :class => "btn btn-primary" %>
39
+              <%= f.submit "Update", class: "btn btn-primary" %>
36 40
             </div>
37 41
           </div>
38 42
         <% end %>
39 43
 
40 44
         <h3>Change password</h3>
41
-        <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put, :class => 'form-horizontal' }) do |f| %>
45
+        <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| %>
42 46
           <%= devise_error_messages! %>
43 47
           <div class="form-group">
44
-            <%= f.label :current_password, :class => 'col-md-4 control-label' %>
48
+            <%= f.label :current_password, class: 'col-md-4 control-label' %>
45 49
             <div class="col-md-6">
46
-              <%= f.password_field :current_password, :class => 'form-control' %>
50
+              <%= f.password_field :current_password, class: 'form-control' %>
47 51
               <span class='help-inline'>We need your current password to confirm your changes.</span>
48 52
             </div>
49 53
           </div>
50 54
 
51 55
           <div class="form-group">
52
-            <%= f.label :password, :class => 'col-md-4 control-label' %>
56
+            <%= f.label :password, class: 'col-md-4 control-label' %>
53 57
             <div class="col-md-6">
54
-              <%= f.password_field :password, :autocomplete => "off", :class => 'form-control' %>
58
+              <%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
55 59
             </div>
56 60
           </div>
57 61
 
58 62
           <div class="form-group">
59
-            <%= f.label :password_confirmation, :class => 'col-md-4 control-label' %>
63
+            <%= f.label :password_confirmation, class: 'col-md-4 control-label' %>
60 64
             <div class="col-md-6">
61
-              <%= f.password_field :password_confirmation, :class => 'form-control' %>
65
+              <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
62 66
             </div>
63 67
           </div>
64 68
 
65 69
           <div class="form-group">
66 70
             <div class="col-md-offset-4 col-md-10">
67
-              <%= f.submit "Update", :class => "btn btn-primary" %>
71
+              <%= f.submit "Update", class: "btn btn-primary" %>
68 72
             </div>
69 73
           </div>
70 74
 
@@ -74,7 +78,7 @@
74 78
 
75 79
         <h3>Cancel my account</h3>
76 80
 
77
-        <p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure?" }, :method => :delete %>.</p>
81
+        <p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p>
78 82
 
79 83
         <%= link_to "Back", :back %>
80 84
       </div>

+ 14 - 13
app/views/devise/registrations/new.html.erb

@@ -3,9 +3,9 @@
3 3
     <div class='col-md-8 col-md-offset-2'>
4 4
       <div class='well'>
5 5
 
6
-        <h2>Sign Up</h2>
6
+        <h2>Sign up</h2>
7 7
 
8
-        <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :class => 'form-horizontal' }) do |f| %>
8
+        <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'form-horizontal' }) do |f| %>
9 9
           <%= devise_error_messages! %>
10 10
           <% if ENV['ON_HEROKU'] && User.count.zero? %>
11 11
           <div class="heroku-instructions">
@@ -31,44 +31,45 @@ bin/setup_heroku
31 31
           <% end %>
32 32
 
33 33
           <div class="form-group">
34
-            <%= f.label :invitation_code, :class => 'col-md-4 control-label' %>
34
+            <%= f.label :invitation_code, class: 'col-md-4 control-label' %>
35 35
             <div class="col-md-6">
36
-              <%= f.text_field :invitation_code, :class => 'form-control' %>
36
+              <%= f.text_field :invitation_code, class: 'form-control' %>
37 37
               <span class="help-inline">We are not yet open to the public.  If you have an invitation code, please enter it here.</span>
38 38
             </div>
39 39
           </div>
40 40
 
41 41
           <div class="form-group">
42
-            <%= f.label :email, :class => 'col-md-4 control-label' %>
42
+            <%= f.label :email, class: 'col-md-4 control-label' %>
43 43
             <div class="col-md-6">
44
-              <%= f.email_field :email, :class => 'form-control' %>
44
+              <%= f.email_field :email, autofocus: true, class: 'form-control' %>
45 45
             </div>
46 46
           </div>
47 47
 
48 48
           <div class="form-group">
49
-            <%= f.label :username, :class => 'col-md-4 control-label' %>
49
+            <%= f.label :username, class: 'col-md-4 control-label' %>
50 50
             <div class="col-md-6">
51
-              <%= f.text_field :username, :class => 'form-control' %>
51
+              <%= f.text_field :username, class: 'form-control' %>
52 52
             </div>
53 53
           </div>
54 54
 
55 55
           <div class="form-group">
56
-            <%= f.label :password, :class => 'col-md-4 control-label' %>
56
+            <%= f.label :password, class: 'col-md-4 control-label' %>
57 57
             <div class="col-md-6">
58
-              <%= f.password_field :password, :autocomplete => "off", :class => 'form-control' %>
58
+              <%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
59
+              <% if @validatable %><span class="help-inline"><%= @minimum_password_length %> characters minimum.</span><% end %>
59 60
             </div>
60 61
           </div>
61 62
 
62 63
           <div class="form-group">
63
-            <%= f.label :password_confirmation, :class => 'col-md-4 control-label' %>
64
+            <%= f.label :password_confirmation, class: 'col-md-4 control-label' %>
64 65
             <div class="col-md-6">
65
-              <%= f.password_field :password_confirmation, :class => 'form-control' %>
66
+              <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
66 67
             </div>
67 68
           </div>
68 69
 
69 70
           <div class="form-group">
70 71
             <div class="col-md-offset-4 col-md-10">
71
-              <%= f.submit "Sign Up", :class => "btn btn-primary" %>
72
+              <%= f.submit "Sign up", class: "btn btn-primary" %>
72 73
             </div>
73 74
           </div>
74 75
 

+ 10 - 10
app/views/devise/sessions/new.html.erb

@@ -2,29 +2,29 @@
2 2
   <div class='col-md-6 col-md-offset-3'>
3 3
     <div class='well'>
4 4
 
5
-      <h2>Sign in</h2>
5
+      <h2>Log in</h2>
6 6
 
7
-      <%= form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { :class => 'form-horizontal', :role => 'form'}) do |f| %>
7
+      <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'form-horizontal', role: 'form'}) do |f| %>
8 8
         <div class="form-group">
9
-          <%= f.label :login, :class => 'col-md-2 col-md-offset-2 control-label' %>
9
+          <%= f.label :login, class: 'col-md-2 col-md-offset-2 control-label' %>
10 10
           <div class="col-md-6">
11
-            <%= f.text_field :login, :class => 'form-control' %>
11
+            <%= f.text_field :login, autofocus: true, class: 'form-control' %>
12 12
           </div>
13 13
         </div>
14 14
 
15 15
         <div class="form-group">
16
-          <%= f.label :password, :class => 'col-md-2 col-md-offset-2 control-label' %>
16
+          <%= f.label :password, class: 'col-md-2 col-md-offset-2 control-label' %>
17 17
           <div class="col-md-6">
18
-            <%= f.password_field :password, :class => 'form-control' %>
18
+            <%= f.password_field :password, class: 'form-control' %>
19 19
           </div>
20 20
         </div>
21 21
 
22 22
         <div class="form-group">
23 23
           <div class="col-md-offset-4 col-md-10">
24
-            <% if devise_mapping.rememberable? %>
24
+            <% if devise_mapping.rememberable? -%>
25 25
               <div class="checkbox">
26 26
                 <label>
27
-                  <%= f.check_box :remember_me %> Remember me
27
+                  <%= f.check_box :remember_me %> <%= f.label :remember_me %>
28 28
                 </label>
29 29
               </div>
30 30
             <% end -%>
@@ -33,7 +33,7 @@
33 33
 
34 34
         <div class="form-group">
35 35
           <div class="col-md-offset-4 col-md-10">
36
-            <%= f.submit "Sign in", :class => "btn btn-default" %>
36
+            <%= f.submit "Log in", class: "btn btn-default" %>
37 37
           </div>
38 38
         </div>
39 39
       <% end %>
@@ -43,4 +43,4 @@
43 43
       <%= render "devise/shared/links" %>
44 44
     </div>
45 45
   </div>
46
-</div>
46
+</div>

+ 2 - 8
app/views/devise/shared/_links.erb

@@ -1,12 +1,12 @@
1 1
 <%- if controller_name != 'sessions' %>
2
-  <%= link_to "Sign in", new_session_path(resource_name) %><br />
2
+  <%= link_to "Log in", new_session_path(resource_name) %><br />
3 3
 <% end -%>
4 4
 
5 5
 <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
6 6
   <%= link_to "Sign up", new_registration_path(resource_name) %><br />
7 7
 <% end -%>
8 8
 
9
-<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
9
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
10 10
   <%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
11 11
 <% end -%>
12 12
 
@@ -17,9 +17,3 @@
17 17
 <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
18 18
   <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
19 19
 <% end -%>
20
-
21
-<%- if devise_mapping.omniauthable? %>
22
-  <%- resource_class.omniauth_providers.each do |provider| %>
23
-    <%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %><br />
24
-  <% end -%>
25
-<% end -%>

+ 5 - 5
app/views/devise/unlocks/new.html.erb

@@ -3,19 +3,19 @@
3 3
     <div class='well'>
4 4
       <h2>Resend unlock instructions</h2>
5 5
 
6
-      <%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %>
6
+      <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'form-horizontal' }) do |f| %>
7 7
         <%= devise_error_messages! %>
8 8
 
9 9
         <div class="form-group">
10
-          <%= f.label :email, :class => 'col-md-2 col-md-offset-2 control-label' %>
10
+         <%= f.label :email, class: 'col-md-2 col-md-offset-2 control-label' %>
11 11
           <div class="col-md-6">
12
-            <%= f.text_field :email, :class => 'form-control' %>
12
+            <%= f.email_field :email, autofocus: true, class: 'form-control' %>
13 13
           </div>
14 14
         </div>
15 15
 
16 16
         <div class="form-group">
17 17
           <div class="col-md-offset-4 col-md-10">
18
-            <%= f.submit "Resend unlock instructions", :class => "btn btn-primary" %>
18
+            <%= f.submit "Resend unlock instructions", class: "btn btn-primary" %>
19 19
           </div>
20 20
         </div>
21 21
       <% end %>
@@ -25,4 +25,4 @@
25 25
       <%= render "devise/shared/links" %>
26 26
     </div>
27 27
   </div>
28
-</div>
28
+</div>

+ 1 - 1
app/views/diagrams/show.html.erb

@@ -9,7 +9,7 @@
9 9
         <h2>Agent Event Flow</h2>
10 10
       </div>
11 11
       <div class="btn-group">
12
-        <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, (params[:scenario_id] ? scenario_path(params[:scenario_id]) : agents_path), class: "btn btn-default" %>
12
+        <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, (params[:scenario_id] ? scenario_path(params[:scenario_id]) : agents_path), class: "btn btn-default" %>
13 13
       </div>
14 14
 
15 15
       <div class='digraph'>

+ 5 - 5
app/views/events/index.html.erb

@@ -18,7 +18,7 @@
18 18
 
19 19
         <% @events.each do |event| %>
20 20
           <% next unless event.agent %>
21
-          <tr>
21
+          <%= content_tag :tr, class: (highlighted?(event.id) ? 'hl' : nil) do %>
22 22
             <td><%= link_to event.agent.name, agent_path(event.agent) %></td>
23 23
             <td title='<%= event.created_at %>'><%= time_ago_in_words event.created_at %> ago</td>
24 24
             <td class='payload'><%= truncate event.payload.to_json, :length => 90, :omission => "" %></td>
@@ -29,19 +29,19 @@
29 29
                 <%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default" %>
30 30
               </div>
31 31
             </td>
32
-          </tr>
32
+          <% end %>
33 33
         <% end %>
34 34
         </table>
35 35
       </div>
36 36
 
37
-      <%= paginate @events, :theme => 'twitter-bootstrap-3' %>
37
+      <%= paginate @events, params: params.slice(:hl), theme: 'twitter-bootstrap-3' %>
38 38
 
39 39
       <br />
40 40
 
41 41
       <% if @agent %>
42 42
         <div class="btn-group">
43
-          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %>
44
-          <%= link_to '<span class="glyphicon glyphicon-random""></span> See all events'.html_safe, events_path, class: "btn btn-default" %>
43
+          <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, agents_path, class: "btn btn-default" %>
44
+          <%= link_to icon_tag('glyphicon-random') + ' See all events'.html_safe, events_path, class: "btn btn-default" %>
45 45
         </div>
46 46
       <% end %>
47 47
     </div>

+ 1 - 1
app/views/events/show.html.erb

@@ -46,7 +46,7 @@
46 46
 
47 47
       <br />
48 48
       <div class="btn-group">
49
-        <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, events_path, class: "btn btn-default" %>
49
+        <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, events_path, class: "btn btn-default" %>
50 50
       </div>
51 51
     </div>
52 52
   </div>

+ 7 - 7
app/views/layouts/_navigation.html.erb

@@ -14,9 +14,9 @@
14 14
     <ul class='nav navbar-nav'>
15 15
       <%= nav_link "Agents", agents_path do %>
16 16
         <ul class='dropdown-menu' role='menu'>
17
-          <%= nav_link "New Agent", new_agent_path, glyphicon: "plus" %>
18
-          <%= nav_link "Run event propagation", propagate_agents_path, method: 'post', glyphicon: "refresh" %>
19
-          <%= nav_link "View Diagram", diagram_path, glyphicon: 'random' %>
17
+          <%= nav_link icon_tag('glyphicon-plus') + " New Agent", new_agent_path %>
18
+          <%= nav_link icon_tag('glyphicon-refresh') + " Run event propagation", propagate_agents_path, method: 'post' %>
19
+          <%= nav_link icon_tag('glyphicon-random') + " View Diagram", diagram_path %>
20 20
         </ul>
21 21
       <% end %>
22 22
       <%= nav_link "Scenarios", scenarios_path %>
@@ -37,22 +37,22 @@
37 37
       
38 38
       <li class='job-indicator' role='pending'>
39 39
         <%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %>
40
-          <span class="badge"><span class="glyphicon glyphicon-refresh icon-white"></span> <span class='number'>0</span></span>
40
+          <span class="badge"><%= icon_tag('glyphicon-refresh', class: 'icon-white') %> <span class='number'>0</span></span>
41 41
         <% end %>
42 42
       </li>
43 43
       <li class='job-indicator' role='awaiting_retry'>
44 44
         <%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %>
45
-          <span class="badge"><span class="glyphicon glyphicon-question-sign icon-yellow"></span> <span class='number'>0</span></span>
45
+          <span class="badge"><%= icon_tag('glyphicon-question-sign', class: 'icon-yellow') %> <span class='number'>0</span></span>
46 46
         <% end %>
47 47
       </li>
48 48
       <li class='job-indicator' role='recent_failures'>
49 49
         <%= link_to current_user.admin? ? jobs_path : '#', class: 'hidden-sm hidden-xs' do %>
50
-          <span class="badge"><span class="glyphicon glyphicon-exclamation-sign icon-white"></span> <span class='number'>0</span></span>
50
+        <span class="badge"><%= icon_tag('glyphicon-exclamation-sign', class: 'icon-white') %> <span class='number'>0</span></span>
51 51
         <% end %>
52 52
       </li>
53 53
       <li id='event-indicator'>
54 54
         <a href="#" class='hidden-sm hidden-xs'>
55
-          <span class="badge"><span class="glyphicon glyphicon-random icon-white"></span> <span class='number'>0</span> new events</span>
55
+          <span class="badge"><%= icon_tag('glyphicon-random', class: 'icon-white') %> <span class='number'>0</span> new events</span>
56 56
         </a>
57 57
       </li>
58 58
     <% end %>

+ 3 - 1
app/views/logs/index.html.erb

@@ -3,7 +3,7 @@
3 3
     <tr>
4 4
       <th>Message</th>
5 5
       <th>When</th>
6
-      <th></th>
6
+      <th width="200px"></th>
7 7
     </tr>
8 8
 
9 9
     <% @logs.each do |log| %>
@@ -24,6 +24,8 @@
24 24
             <% else %>
25 25
               <%= link_to 'Event Out', '#', class: "btn btn-default disabled" %>
26 26
             <% end %>
27
+
28
+            <%= link_to 'Details', '#', :class => "btn btn-default show-log-details", :data => { :'modal-title' => log.level >= 4 ? 'Error' : 'Info', :'modal-content' => log.message } %>
27 29
           </div>
28 30
         </td>
29 31
       </tr>

+ 2 - 2
app/views/scenario_imports/new.html.erb

@@ -26,7 +26,7 @@
26 26
 
27 27
   <div class="row">
28 28
     <div class="col-md-12">
29
-      <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
29
+      <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenarios_path, class: "btn btn-default" %>
30 30
     </div>
31 31
   </div>
32
-</div>
32
+</div>

+ 2 - 2
app/views/scenarios/edit.html.erb

@@ -13,9 +13,9 @@
13 13
 
14 14
       <div class="row">
15 15
         <div class="col-md-12">
16
-          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
16
+          <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenarios_path, class: "btn btn-default" %>
17 17
         </div>
18 18
       </div>
19 19
     </div>
20 20
   </div>
21
-</div>
21
+</div>

+ 2 - 2
app/views/scenarios/index.html.erb

@@ -43,8 +43,8 @@
43 43
       <br/>
44 44
 
45 45
       <div class="btn-group">
46
-        <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %>
47
-        <%= link_to '<span class="glyphicon glyphicon-plus"></span> Import Scenario'.html_safe, new_scenario_imports_path, class: "btn btn-default" %>
46
+        <%= link_to icon_tag('glyphicon-plus') + ' New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %>
47
+        <%= link_to icon_tag('glyphicon-cloud-upload') + ' Import Scenario'.html_safe, new_scenario_imports_path, class: "btn btn-default" %>
48 48
       </div>
49 49
     </div>
50 50
   </div>

+ 2 - 2
app/views/scenarios/new.html.erb

@@ -13,9 +13,9 @@
13 13
 
14 14
       <div class="row">
15 15
         <div class="col-md-12">
16
-          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
16
+          <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenarios_path, class: "btn btn-default" %>
17 17
         </div>
18 18
       </div>
19 19
     </div>
20 20
   </div>
21
-</div>
21
+</div>

+ 1 - 1
app/views/scenarios/share.html.erb

@@ -25,7 +25,7 @@
25 25
 
26 26
       <div class="row">
27 27
         <div class="col-md-12">
28
-          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenario_path(@scenario), class: "btn btn-default" %>
28
+          <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenario_path(@scenario), class: "btn btn-default" %>
29 29
         </div>
30 30
       </div>
31 31
     </div>

+ 6 - 6
app/views/scenarios/show.html.erb

@@ -15,14 +15,14 @@
15 15
       <br/>
16 16
 
17 17
       <div class="btn-group">
18
-        <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
19
-        <%= link_to '<span class="glyphicon glyphicon-random"></span> View Diagram'.html_safe, scenario_diagram_path(@scenario), class: "btn btn-default" %>
20
-        <%= link_to '<span class="glyphicon glyphicon-edit"></span> Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %>
18
+        <%= link_to icon_tag('glyphicon-chevron-left') + ' Back', scenarios_path, class: "btn btn-default" %>
19
+        <%= link_to icon_tag('glyphicon-random') + ' View Diagram', scenario_diagram_path(@scenario), class: "btn btn-default" %>
20
+        <%= link_to icon_tag('glyphicon-edit') + ' Edit', edit_scenario_path(@scenario), class: "btn btn-default" %>
21 21
         <% if @scenario.source_url.present? %>
22
-          <%= link_to '<span class="glyphicon glyphicon-plus"></span> Update'.html_safe, new_scenario_imports_path(:url => @scenario.source_url), class: "btn btn-default" %>
22
+          <%= link_to icon_tag('glyphicon-plus') + ' Update', new_scenario_imports_path(url: @scenario.source_url), class: "btn btn-default" %>
23 23
         <% end %>
24
-        <%= link_to '<span class="glyphicon glyphicon-share-alt"></span> Share'.html_safe, share_scenario_path(@scenario), class: "btn btn-default" %>
25
-        <%= link_to '<span class="glyphicon glyphicon-trash"></span> Delete'.html_safe, scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it.  Are you sure?" }, class: "btn btn-default" %>
24
+        <%= link_to icon_tag('glyphicon-share-alt') + ' Share', share_scenario_path(@scenario), class: "btn btn-default" %>
25
+        <%= link_to icon_tag('glyphicon-trash') + ' Delete', scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it.  Are you sure?" }, class: "btn btn-default" %>
26 26
       </div>
27 27
     </div>
28 28
   </div>

+ 3 - 3
app/views/services/index.html.erb

@@ -12,7 +12,7 @@
12 12
         for guidance.
13 13
       </p>
14 14
       <%- Devise.omniauth_providers.each { |provider| -%>
15
-        <p><%= link_to user_omniauth_authorize_path(provider), class: "btn btn-default btn-auth btn-auth-#{provider}" do %><%= icon_for_service(provider) %><span>Authenticate with <%= t("devise.omniauth_providers.#{provider}") %></span><% end %></p>
15
+        <p><%= omniauth_button(provider) %></p>
16 16
       <%- } -%>
17 17
       <hr>
18 18
 
@@ -27,9 +27,9 @@
27 27
 
28 28
         <% @services.each do |service| %>
29 29
           <tr>
30
-            <td><%= service.provider %></td>
30
+            <td><%= omniauth_provider_name(service.provider) %></td>
31 31
             <td><%= service.name %></td>
32
-            <td><%= service.global ? 'Yes' : 'No' %></td>
32
+            <td><%= yes_no(service.global) %></td>
33 33
             <td>
34 34
               <div class="btn-group btn-group-xs">
35 35
                 <% if service.global %>

+ 2 - 2
app/views/user_credentials/edit.html.erb

@@ -13,9 +13,9 @@
13 13
 
14 14
       <div class="row">
15 15
         <div class="col-md-12">
16
-          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, user_credentials_path, class: "btn btn-default" %>
16
+          <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, user_credentials_path, class: "btn btn-default" %>
17 17
         </div>
18 18
       </div>
19 19
     </div>
20 20
   </div>
21
-</div>
21
+</div>

+ 1 - 1
app/views/user_credentials/index.html.erb

@@ -38,7 +38,7 @@
38 38
 
39 39
       <div class="btn-group">
40 40
         <%= link_to new_user_credential_path, class: "btn btn-default" do %><span class="glyphicon glyphicon-plus"></span> New Credential<% end %>
41
-        <%= link_to user_credentials_path(format: :json), class: "btn btn-default" do %><span class="glyphicon glyphicon-download-alt"></span> Download Credentials<% end %>
41
+        <%= link_to user_credentials_path(format: :json), class: "btn btn-default" do %><span class="glyphicon glyphicon-cloud-download"></span> Download Credentials<% end %>
42 42
       </div>
43 43
     </div>
44 44
   </div>

+ 2 - 2
app/views/user_credentials/new.html.erb

@@ -13,9 +13,9 @@
13 13
 
14 14
       <div class="row">
15 15
         <div class="col-md-12">
16
-          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, user_credentials_path, class: "btn btn-default" %>
16
+          <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, user_credentials_path, class: "btn btn-default" %>
17 17
         </div>
18 18
       </div>
19 19
     </div>
20 20
   </div>
21
-</div>
21
+</div>

+ 2 - 2
bin/schedule.rb

@@ -11,5 +11,5 @@ unless defined?(Rails)
11 11
   exit 1
12 12
 end
13 13
 
14
-scheduler = HuginnScheduler.new
15
-scheduler.run!
14
+scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'])
15
+scheduler.run!

+ 1 - 1
bin/threaded.rb

@@ -33,7 +33,7 @@ end
33 33
 
34 34
 threads << Thread.new do
35 35
   safely do
36
-    @scheduler = HuginnScheduler.new
36
+    @scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY'])
37 37
     @scheduler.run!
38 38
     puts "Scheduler stopped ..."
39 39
   end

+ 1 - 1
config/application.rb

@@ -13,7 +13,7 @@ module Huginn
13 13
     # -- all .rb files in that directory are automatically loaded.
14 14
 
15 15
     # Custom directories with classes and modules you want to be autoloadable.
16
-    config.autoload_paths += %W(#{config.root}/lib)
16
+    config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters)
17 17
 
18 18
     # Activate observers that should always be running.
19 19
     # config.active_record.observers = :cacher, :garbage_collector, :forum_observer

+ 1 - 1
config/boot.rb

@@ -3,4 +3,4 @@ require 'rubygems'
3 3
 # Set up gems listed in the Gemfile.
4 4
 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 5
 
6
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
6
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

+ 6 - 0
config/initializers/delayed_job.rb

@@ -7,3 +7,9 @@ Delayed::Worker.delay_jobs = !Rails.env.test?
7 7
 
8 8
 # Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log'))
9 9
 # Delayed::Worker.logger.level = Logger::DEBUG
10
+
11
+class Delayed::Job
12
+  scope :pending, ->{ where("locked_at IS NULL AND attempts = 0") }
13
+  scope :awaiting_retry, ->{ where("failed_at IS NULL AND attempts > 0") }
14
+  scope :failed, -> { where("failed_at IS NOT NULL") }
15
+end

+ 54 - 28
config/initializers/devise.rb

@@ -5,10 +5,10 @@ Devise.setup do |config|
5 5
   # Configure the e-mail address which will be shown in Devise::Mailer,
6 6
   # note that it will be overwritten if you use your own mailer class
7 7
   # with default "from" parameter.
8
-  config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com"
8
+  config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
9 9
 
10 10
   # Configure the class responsible to send e-mails.
11
-  # config.mailer = "Devise::Mailer"
11
+  # config.mailer = 'Devise::Mailer'
12 12
 
13 13
   # ==> ORM configuration
14 14
   # Load and configure the ORM. Supports :active_record (default) and
@@ -49,17 +49,18 @@ Devise.setup do |config|
49 49
   # enable it only for database (email + password) authentication.
50 50
   # config.params_authenticatable = true
51 51
 
52
-  # Tell if authentication through HTTP Basic Auth is enabled. False by default.
52
+  # Tell if authentication through HTTP Auth is enabled. False by default.
53 53
   # It can be set to an array that will enable http authentication only for the
54
-  # given strategies, for example, `config.http_authenticatable = [:token]` will
55
-  # enable it only for token authentication.
54
+  # given strategies, for example, `config.http_authenticatable = [:database]` will
55
+  # enable it only for database authentication. The supported strategies are:
56
+  # :database      = Support basic authentication with authentication key + password
56 57
   # config.http_authenticatable = false
57 58
 
58
-  # If http headers should be returned for AJAX requests. True by default.
59
+  # If 401 status code should be returned for AJAX requests. True by default.
59 60
   # config.http_authenticatable_on_xhr = true
60 61
 
61
-  # The realm used in Http Basic Authentication. "Application" by default.
62
-  # config.http_authentication_realm = "Application"
62
+  # The realm used in Http Basic Authentication. 'Application' by default.
63
+  # config.http_authentication_realm = 'Application'
63 64
 
64 65
   # It will change confirmation, password recovery and other workflows
65 66
   # to behave the same regardless if the e-mail provided was right or wrong.
@@ -67,10 +68,10 @@ Devise.setup do |config|
67 68
   # config.paranoid = true
68 69
 
69 70
   # By default Devise will store the user in session. You can skip storage for
70
-  # :http_auth and :token_auth by adding those symbols to the array below.
71
+  # particular strategies by setting this option.
71 72
   # Notice that if you are skipping storage for all authentication paths, you
72 73
   # may want to disable generating routes to Devise's sessions controller by
73
-  # passing :skip => :sessions to `devise_for` in your config/routes.rb
74
+  # passing skip: :sessions to `devise_for` in your config/routes.rb
74 75
   config.skip_session_storage = [:http_auth]
75 76
 
76 77
   # By default, Devise cleans up the CSRF token on authentication to
@@ -85,7 +86,9 @@ Devise.setup do |config|
85 86
   #
86 87
   # Limiting the stretches to just one in testing will increase the performance of
87 88
   # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
88
-  # a value less than 10 in other environments.
89
+  # a value less than 10 in other environments. Note that, for bcrypt (the default
90
+  # encryptor), the cost increases exponentially with the number of stretches (e.g.
91
+  # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
89 92
   config.stretches = Rails.env.test? ? 1 : 10
90 93
 
91 94
   # Setup a pepper to generate the encrypted password.
@@ -93,16 +96,24 @@ Devise.setup do |config|
93 96
 
94 97
   # ==> Configuration for :confirmable
95 98
   # A period that the user is allowed to access the website even without
96
-  # confirming his account. For instance, if set to 2.days, the user will be
97
-  # able to access the website for two days without confirming his account,
99
+  # confirming their account. For instance, if set to 2.days, the user will be
100
+  # able to access the website for two days without confirming their account,
98 101
   # access will be blocked just in the third day. Default is 0.days, meaning
99
-  # the user cannot access the website without confirming his account.
102
+  # the user cannot access the website without confirming their account.
100 103
   # config.allow_unconfirmed_access_for = 2.days
101 104
 
105
+  # A period that the user is allowed to confirm their account before their
106
+  # token becomes invalid. For example, if set to 3.days, the user can confirm
107
+  # their account within 3 days after the mail was sent, but on the fourth day
108
+  # their account can't be confirmed with the token any more.
109
+  # Default is nil, meaning there is no restriction on how long a user can take
110
+  # before confirming their account.
111
+  # config.confirm_within = 3.days
112
+
102 113
   # If true, requires any email changes to be confirmed (exactly the same way as
103 114
   # initial account confirmation) to be applied. Requires additional unconfirmed_email
104
-  # db field (see migrations). Until confirmed new email is stored in
105
-  # unconfirmed email column, and copied to email column on successful confirmation.
115
+  # db field (see migrations). Until confirmed, new email is stored in
116
+  # unconfirmed_email column, and copied to email column on successful confirmation.
106 117
   config.reconfirmable = true
107 118
 
108 119
   # Defines which key will be used when confirming an account
@@ -112,23 +123,26 @@ Devise.setup do |config|
112 123
   # The time the user will be remembered without asking for credentials again.
113 124
   config.remember_for = 4.weeks
114 125
 
126
+  # Invalidates all the remember me tokens when the user signs out.
127
+  config.expire_all_remember_me_on_sign_out = true
128
+
115 129
   # If true, extends the user's remember period when remembered via cookie.
116 130
   # config.extend_remember_period = false
117 131
 
118 132
   # Options to be passed to the created cookie. For instance, you can set
119
-  # :secure => true in order to force SSL only cookies.
133
+  # secure: true in order to force SSL only cookies.
120 134
   if Rails.env.production?
121
-    config.rememberable_options = { :secure => true }
135
+    config.rememberable_options = { secure: true }
122 136
   else
123 137
     config.rememberable_options = { }
124 138
   end
125 139
 
126 140
   # ==> Configuration for :validatable
127
-  # Range for password length. Default is 6..128.
128
-  # config.password_length = 6..128
141
+  # Range for password length.
142
+  config.password_length = 8..128
129 143
 
130 144
   # Email regex used to validate email formats. It simply asserts that
131
-  # an one (and only one) @ exists in the given string. This is mainly
145
+  # one (and only one) @ exists in the given string. This is mainly
132 146
   # to give user feedback and not to assert the e-mail validity.
133 147
   # config.email_regexp = /\A[^@]+@[^@]+\z/
134 148
 
@@ -136,7 +150,7 @@ Devise.setup do |config|
136 150
   # The time you want to timeout the user session without activity. After this
137 151
   # time the user will be asked for credentials again. Default is 30 minutes.
138 152
   # config.timeout_in = 30.minutes
139
-  
153
+
140 154
   # If true, expires auth token on session timeout.
141 155
   # config.expire_auth_token_on_timeout = false
142 156
 
@@ -163,6 +177,9 @@ Devise.setup do |config|
163 177
   # Time interval to unlock the account if :time is enabled as unlock_strategy.
164 178
   config.unlock_in = 1.hour
165 179
 
180
+  # Warn on the last attempt before the account is locked.
181
+  # config.last_attempt_warning = true
182
+
166 183
   # ==> Configuration for :recoverable
167 184
   #
168 185
   # Defines which key will be used when recovering the password for an account
@@ -178,7 +195,9 @@ Devise.setup do |config|
178 195
   # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1,
179 196
   # :authlogic_sha512 (then you should set stretches above to 20 for default behavior)
180 197
   # and :restful_authentication_sha1 (then you should set stretches to 10, and copy
181
-  # REST_AUTH_SITE_KEY to pepper)
198
+  # REST_AUTH_SITE_KEY to pepper).
199
+  #
200
+  # Require the `devise-encryptable` gem when using anything other than bcrypt
182 201
   # config.encryptor = :sha512
183 202
 
184 203
   # ==> Scopes configuration
@@ -204,7 +223,7 @@ Devise.setup do |config|
204 223
   # should add them to the navigational formats lists.
205 224
   #
206 225
   # The "*/*" below is required to match Internet Explorer requests.
207
-  # config.navigational_formats = ["*/*", :html]
226
+  # config.navigational_formats = ['*/*', :html]
208 227
 
209 228
   # The default HTTP method used to sign out a resource. Default is :delete.
210 229
   config.sign_out_via = :get
@@ -212,7 +231,8 @@ Devise.setup do |config|
212 231
   # ==> OmniAuth
213 232
   # Add a new OmniAuth provider. Check the wiki for more information on setting
214 233
   # up on your models and hooks.
215
-  # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo'
234
+  # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
235
+
216 236
   if defined?(OmniAuth::Strategies::Twitter) &&
217 237
      (key = ENV["TWITTER_OAUTH_KEY"]).present? &&
218 238
      (secret = ENV["TWITTER_OAUTH_SECRET"]).present?
@@ -237,13 +257,19 @@ Devise.setup do |config|
237 257
     config.omniauth :github, key, secret
238 258
   end
239 259
 
260
+  if defined?(OmniAuth::Strategies::Dropbox) &&
261
+     (key = ENV["DROPBOX_OAUTH_KEY"]).present? &&
262
+     (secret = ENV["DROPBOX_OAUTH_SECRET"]).present?
263
+    config.omniauth :dropbox, key, secret
264
+  end
265
+
240 266
   # ==> Warden configuration
241 267
   # If you want to use other strategies, that are not supported by Devise, or
242 268
   # change the failure app, you can configure them inside the config.warden block.
243 269
   #
244 270
   # config.warden do |manager|
245 271
   #   manager.intercept_401 = false
246
-  #   manager.default_strategies(:scope => :user).unshift :some_external_strategy
272
+  #   manager.default_strategies(scope: :user).unshift :some_external_strategy
247 273
   # end
248 274
 
249 275
   # ==> Mountable engine configurations
@@ -251,13 +277,13 @@ Devise.setup do |config|
251 277
   # is mountable, there are some extra configurations to be taken into account.
252 278
   # The following options are available, assuming the engine is mounted as:
253 279
   #
254
-  #     mount MyEngine, at: "/my_engine"
280
+  #     mount MyEngine, at: '/my_engine'
255 281
   #
256 282
   # The router that invoked `devise_for`, in the example above, would be:
257 283
   # config.router_name = :my_engine
258 284
   #
259 285
   # When using omniauth, Devise cannot automatically set Omniauth path,
260 286
   # so you need to do it manually. For the users scope, it would be:
261
-  # config.omniauth_path_prefix = "/my_engine/users/auth"
287
+  # config.omniauth_path_prefix = '/my_engine/users/auth'
262 288
   config.omniauth_path_prefix = "/auth"
263 289
 end

+ 55 - 52
config/locales/devise.en.yml

@@ -1,63 +1,66 @@
1 1
 # Additional translations at https://github.com/plataformatec/devise/wiki/I18n
2 2
 
3 3
 en:
4
+  devise:
5
+    confirmations:
6
+      confirmed: "Your email address has been successfully confirmed."
7
+      send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
8
+      send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
9
+    failure:
10
+      already_authenticated: "You are already signed in."
11
+      inactive: "Your account is not activated yet."
12
+      invalid: "Invalid %{authentication_keys} or password."
13
+      locked: "Your account is locked."
14
+      last_attempt: "You have one more attempt before your account is locked."
15
+      not_found_in_database: "Invalid %{authentication_keys} or password."
16
+      timeout: "Your session expired. Please sign in again to continue."
17
+      unauthenticated: "You need to sign in or sign up before continuing."
18
+      unconfirmed: "You have to confirm your email address before continuing."
19
+    mailer:
20
+      confirmation_instructions:
21
+        subject: "Confirmation instructions"
22
+      reset_password_instructions:
23
+        subject: "Reset password instructions"
24
+      unlock_instructions:
25
+        subject: "Unlock instructions"
26
+    omniauth_callbacks:
27
+      failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
28
+      success: "Successfully authenticated from %{kind} account."
29
+    omniauth_providers:
30
+      twitter: "Twitter"
31
+      tumblr: "Tumblr"
32
+      github: "GitHub"
33
+      37signals: "37Signals (Basecamp)"
34
+      dropbox: "Dropbox"
35
+    passwords:
36
+      no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
37
+      send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
38
+      send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
39
+      updated: "Your password has been changed successfully. You are now signed in."
40
+      updated_not_active: "Your password has been changed successfully."
41
+    registrations:
42
+      destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
43
+      signed_up: "Welcome! You have signed up successfully."
44
+      signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
45
+      signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
46
+      signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
47
+      update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
48
+      updated: "Your account has been updated successfully."
49
+    sessions:
50
+      signed_in: "Signed in successfully."
51
+      signed_out: "Signed out successfully."
52
+      already_signed_out: "Signed out successfully."
53
+    unlocks:
54
+      send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
55
+      send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
56
+      unlocked: "Your account has been unlocked successfully. Please sign in to continue."
4 57
   errors:
5 58
     messages:
59
+      already_confirmed: "was already confirmed, please try signing in"
60
+      confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
6 61
       expired: "has expired, please request a new one"
7 62
       not_found: "not found"
8
-      already_confirmed: "was already confirmed, please try signing in"
9 63
       not_locked: "was not locked"
10 64
       not_saved:
11 65
         one: "1 error prohibited this %{resource} from being saved:"
12 66
         other: "%{count} errors prohibited this %{resource} from being saved:"
13
-
14
-  devise:
15
-    failure:
16
-      already_authenticated: 'You are already signed in.'
17
-      unauthenticated: 'You need to sign in or sign up before continuing.'
18
-      unconfirmed: 'You have to confirm your account before continuing.'
19
-      locked: 'Your account is locked.'
20
-      invalid: 'Invalid login or password.'
21
-      invalid_token: 'Invalid authentication token.'
22
-      timeout: 'Your session expired, please sign in again to continue.'
23
-      inactive: 'Your account was not activated yet.'
24
-    sessions:
25
-      signed_in: 'Signed in successfully.'
26
-      signed_out: 'Signed out successfully.'
27
-    passwords:
28
-      send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
29
-      updated: 'Your password was changed successfully. You are now signed in.'
30
-      updated_not_active: 'Your password was changed successfully.'
31
-      send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
32
-      no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
33
-    confirmations:
34
-      send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.'
35
-      send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.'
36
-      confirmed: 'Your account was successfully confirmed. You are now signed in.'
37
-    registrations:
38
-      signed_up: 'Welcome! You have signed up successfully.'
39
-      signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.'
40
-      signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.'
41
-      signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.'
42
-      updated: 'You updated your account successfully.'
43
-      update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address."
44
-      destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
45
-    unlocks:
46
-      send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
47
-      unlocked: 'Your account has been unlocked successfully. Please sign in to continue.'
48
-      send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.'
49
-    omniauth_callbacks:
50
-      success: 'Successfully authenticated from %{kind} account.'
51
-      failure: 'Could not authenticate you from %{kind} because "%{reason}".'
52
-    omniauth_providers:
53
-      twitter: 'Twitter'
54
-      tumblr: 'Tumblr'
55
-      github: 'GitHub'
56
-      37signals: '37Signals (Basecamp)'
57
-    mailer:
58
-      confirmation_instructions:
59
-        subject: 'Confirmation instructions'
60
-      reset_password_instructions:
61
-        subject: 'Reset password instructions'
62
-      unlock_instructions:
63
-        subject: 'Unlock Instructions'

+ 5 - 3
config/routes.rb

@@ -11,6 +11,8 @@ Huginn::Application.routes.draw do
11 11
       post :propagate
12 12
       get :type_details
13 13
       get :event_descriptions
14
+      post :validate
15
+      post :complete
14 16
     end
15 17
 
16 18
     resources :logs, :only => [:index] do
@@ -66,9 +68,9 @@ Huginn::Application.routes.draw do
66 68
   post  "/users/:user_id/webhooks/:agent_id/:secret" => "web_requests#handle_request" # legacy
67 69
   post  "/users/:user_id/update_location/:secret" => "web_requests#update_location" # legacy
68 70
 
69
-  match '/auth/:provider/callback', to: 'services#callback',
70
-        via: [:get, :post] #, constraints: { provider: Regexp.union(Devise.omniauth_providers.map(&:to_s)) }
71
-  devise_for :users, :sign_out_via => [ :post, :delete ]
71
+  devise_for :users,
72
+             controllers: { omniauth_callbacks: 'omniauth_callbacks' },
73
+             sign_out_via: [:post, :delete]
72 74
 
73 75
   get "/about" => "home#about"
74 76
   root :to => "home#index"

+ 1 - 1
deployment/site-cookbooks/huginn_production/files/default/unicorn.rb

@@ -20,7 +20,7 @@ before_fork do |server, worker|
20 20
   defined?(ActiveRecord::Base) and
21 21
     ActiveRecord::Base.connection.disconnect!
22 22
   old_pid = "#{server.config[:pid]}.oldbin"
23
-  if File.exists?(old_pid) && server.pid != old_pid
23
+  if File.exist?(old_pid) && server.pid != old_pid
24 24
     begin
25 25
       Process.kill("QUIT", File.read(old_pid).to_i)
26 26
     rescue Errno::ENOENT, Errno::ESRCH

+ 1 - 1
doc/deployment/unicorn/production.rb

@@ -19,7 +19,7 @@ pid '/home/you/app/shared/pids/unicorn.pid'
19 19
 before_fork do |server, worker|
20 20
   ActiveRecord::Base.connection.disconnect!
21 21
   old_pid = "#{server.config[:pid]}.oldbin"
22
-  if File.exists?(old_pid) && server.pid != old_pid
22
+  if File.exist?(old_pid) && server.pid != old_pid
23 23
     begin
24 24
       Process.kill("QUIT", File.read(old_pid).to_i)
25 25
     rescue Errno::ENOENT, Errno::ESRCH

+ 1 - 1
docker/README.md

@@ -90,7 +90,7 @@ These are:
90 90
     HUGINN_RAILS_ENV
91 91
     HUGINN_FORCE_SSL
92 92
     HUGINN_INVITATION_CODE
93
-    HUGINN_SMTP_DOMAIM
93
+    HUGINN_SMTP_DOMAIN
94 94
     HUGINN_SMTP_USER_NAME
95 95
     HUGINN_SMTP_PASSWORD
96 96
     HUGINN_SMTP_SERVER

+ 2 - 2
lib/huginn_scheduler.rb

@@ -96,8 +96,8 @@ class HuginnScheduler
96 96
   FAILED_JOBS_TO_KEEP = 100
97 97
   attr_accessor :mutex
98 98
 
99
-  def initialize
100
-    @rufus_scheduler = Rufus::Scheduler.new
99
+  def initialize(options = {})
100
+    @rufus_scheduler = Rufus::Scheduler.new(options)
101 101
     self.mutex = Mutex.new
102 102
   end
103 103
 

+ 56 - 0
spec/concerns/form_configurable_spec.rb

@@ -0,0 +1,56 @@
1
+require 'spec_helper'
2
+
3
+describe FormConfigurable do
4
+  class Agent1
5
+    include FormConfigurable
6
+
7
+    def validate_test
8
+      true
9
+    end
10
+
11
+    def complete_test
12
+      [{name: 'test', value: 1234}]
13
+    end
14
+  end
15
+
16
+  class Agent2 < Agent
17
+  end
18
+
19
+  before(:all) do
20
+    @agent1 = Agent1.new
21
+    @agent2 = Agent2.new
22
+  end
23
+
24
+  it "#is_form_configurable" do
25
+    expect(@agent1.is_form_configurable?).to be true
26
+    expect(@agent2.is_form_configurable?).to be false
27
+  end
28
+
29
+  describe "#validete_option" do
30
+    it "should call the validation method if it is defined" do
31
+      expect(@agent1.validate_option('test')).to be true
32
+    end
33
+
34
+    it "should return false of the method is undefined" do
35
+      expect(@agent1.validate_option('undefined')).to be false
36
+    end
37
+  end
38
+
39
+  it "#complete_option" do
40
+    expect(@agent1.complete_option('test')).to eq [{name: 'test', value: 1234}]
41
+  end
42
+
43
+  describe "#form_configurable" do
44
+    it "should raise an ArgumentError for invalid  options" do
45
+      expect { Agent1.form_configurable(:test, invalid: true) }.to raise_error(ArgumentError)
46
+    end
47
+
48
+    it "should raise an ArgumentError when not providing an array with type: array" do
49
+      expect { Agent1.form_configurable(:test, type: :array, values: 1) }.to raise_error(ArgumentError)
50
+    end
51
+
52
+    it "should not require any options for the default values" do
53
+      expect { Agent1.form_configurable(:test) }.to change(Agent1, :form_configurable_attributes).by(['test'])
54
+    end
55
+  end
56
+end

+ 40 - 0
spec/controllers/agents_controller_spec.rb

@@ -307,4 +307,44 @@ describe AgentsController do
307 307
       expect(response).to redirect_to scenario_path(scenarios(:bob_weather))
308 308
     end
309 309
   end
310
+
311
+  describe "#form_configurable actions" do
312
+    before(:each) do
313
+      @params = {attribute: 'auth_token', agent: valid_attributes(:type => "Agents::HipchatAgent", options: {auth_token: '12345'})}
314
+      sign_in users(:bob)
315
+    end
316
+    describe "POST validate" do
317
+
318
+      it "returns with status 200 when called with a valid option" do
319
+        any_instance_of(Agents::HipchatAgent) do |klass|
320
+          stub(klass).validate_option { true }
321
+        end
322
+
323
+        post :validate, @params
324
+        expect(response.status).to eq 200
325
+      end
326
+
327
+      it "returns with status 403 when called with an invalid option" do
328
+        any_instance_of(Agents::HipchatAgent) do |klass|
329
+          stub(klass).validate_option { false }
330
+        end
331
+
332
+        post :validate, @params
333
+        expect(response.status).to eq 403
334
+      end
335
+    end
336
+
337
+    describe "POST complete" do
338
+      it "callsAgent#complete_option and renders json" do
339
+        any_instance_of(Agents::HipchatAgent) do |klass|
340
+          stub(klass).complete_option { [{name: 'test', value: 1}] }
341
+        end
342
+
343
+        post :complete, @params
344
+        expect(response.status).to eq 200
345
+        expect(response.header['Content-Type']).to include('application/json')
346
+
347
+      end
348
+    end
349
+  end
310 350
 end

+ 26 - 0
spec/controllers/omniauth_callbacks_controller_spec.rb

@@ -0,0 +1,26 @@
1
+require 'spec_helper'
2
+
3
+describe OmniauthCallbacksController do
4
+  before do
5
+    sign_in users(:bob)
6
+    OmniAuth.config.test_mode = true
7
+    request.env["devise.mapping"] = Devise.mappings[:user]
8
+    request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
9
+  end
10
+
11
+  describe "accepting a callback url" do
12
+    it "should update the user's credentials" do
13
+      expect {
14
+        get :twitter
15
+      }.to change { users(:bob).services.count }.by(1)
16
+    end
17
+
18
+    # it "should work with an unknown provider (for now)" do
19
+    #   request.env["omniauth.auth"]['provider'] = 'unknown'
20
+    #   expect {
21
+    #     get :unknown
22
+    #   }.to change { users(:bob).services.count }.by(1)
23
+    #   expect(users(:bob).services.first.provider).to eq('unknown')
24
+    # end
25
+  end
26
+end

+ 1 - 1
spec/controllers/scenarios_controller_spec.rb

@@ -28,7 +28,7 @@ describe ScenariosController do
28 28
 
29 29
     it "loads Agents for the requested Scenario" do
30 30
       get :show, :id => scenarios(:bob_weather).to_param
31
-      expect(assigns(:agents).pluck(:id)).to eq(scenarios(:bob_weather).agents.pluck(:id))
31
+      expect(assigns(:agents).pluck(:id).sort).to eq(scenarios(:bob_weather).agents.pluck(:id).sort)
32 32
     end
33 33
   end
34 34
 

+ 0 - 18
spec/controllers/services_controller_spec.rb

@@ -3,8 +3,6 @@ require 'spec_helper'
3 3
 describe ServicesController do
4 4
   before do
5 5
     sign_in users(:bob)
6
-    OmniAuth.config.test_mode = true
7
-    request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json')))
8 6
   end
9 7
 
10 8
   describe "GET index" do
@@ -39,20 +37,4 @@ describe ServicesController do
39 37
       }.to raise_error(ActiveRecord::RecordNotFound)
40 38
     end
41 39
   end
42
-
43
-  describe "accepting a callback url" do
44
-    it "should update the user's credentials" do
45
-      expect {
46
-        get :callback, provider: 'twitter'
47
-      }.to change { users(:bob).services.count }.by(1)
48
-    end
49
-
50
-    it "should work with an unknown provider (for now)" do
51
-      request.env["omniauth.auth"]['provider'] = 'unknown'
52
-      expect {
53
-        get :callback, provider: 'unknown'
54
-      }.to change { users(:bob).services.count }.by(1)
55
-      expect(users(:bob).services.first.provider).to eq('unknown')
56
-    end
57
-  end
58 40
 end

+ 2 - 0
spec/env.test

@@ -5,4 +5,6 @@ TUMBLR_OAUTH_KEY=tumblroauthsecret
5 5
 TUMBLR_OAUTH_SECRET=tumblroauthsecret
6 6
 THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY
7 7
 THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET
8
+DROPBOX_OAUTH_KEY=dropboxoauthkey
9
+DROPBOX_OAUTH_SECRET=dropboxoauthsecret
8 10
 FAILED_JOBS_TO_KEEP=2

+ 62 - 13
spec/helpers/application_helper_spec.rb

@@ -1,6 +1,32 @@
1 1
 require 'spec_helper'
2 2
 
3 3
 describe ApplicationHelper do
4
+  describe '#icon_tag' do
5
+    it 'returns a Glyphicon icon element' do
6
+      icon = icon_tag('glyphicon-help')
7
+      expect(icon).to be_html_safe
8
+      expect(Nokogiri(icon).at('span.glyphicon.glyphicon-help')).to be_a Nokogiri::XML::Element
9
+    end
10
+
11
+    it 'returns a Glyphicon icon element with an addidional class' do
12
+      icon = icon_tag('glyphicon-help', class: 'text-info')
13
+      expect(icon).to be_html_safe
14
+      expect(Nokogiri(icon).at('span.glyphicon.glyphicon-help.text-info')).to be_a Nokogiri::XML::Element
15
+    end
16
+
17
+    it 'returns a FontAwesome icon element' do
18
+      icon = icon_tag('fa-copy')
19
+      expect(icon).to be_html_safe
20
+      expect(Nokogiri(icon).at('i.fa.fa-copy')).to be_a Nokogiri::XML::Element
21
+    end
22
+
23
+    it 'returns a FontAwesome icon element' do
24
+      icon = icon_tag('fa-copy', class: 'text-info')
25
+      expect(icon).to be_html_safe
26
+      expect(Nokogiri(icon).at('i.fa.fa-copy.text-info')).to be_a Nokogiri::XML::Element
27
+    end
28
+  end
29
+
4 30
   describe '#nav_link' do
5 31
     it 'returns a nav link' do
6 32
       stub(self).current_page?('/things') { false }
@@ -9,15 +35,6 @@ describe ApplicationHelper do
9 35
       expect(a.text.strip).to eq('Things')
10 36
     end
11 37
 
12
-    it 'returns a nav link with a glyphicon' do
13
-      stub(self).current_page?('/things') { false }
14
-      nav = nav_link('Things', '/things', glyphicon: 'help')
15
-      expect(nav).to be_html_safe
16
-      a = Nokogiri(nav).at('li:not(.active) > a[href="/things"]')
17
-      expect(a.at('span.glyphicon.glyphicon-help')).to be_a Nokogiri::XML::Element
18
-      expect(a.text.strip).to eq('Things')
19
-    end
20
-
21 38
     it 'returns an active nav link' do
22 39
       stub(self).current_page?('/things') { true }
23 40
       nav = nav_link('Things', '/things')
@@ -121,26 +138,58 @@ describe ApplicationHelper do
121 138
     end
122 139
   end
123 140
 
124
-  describe '#icon_for_service' do
141
+  describe '#omniauth_provider_icon' do
125 142
     it 'returns a correct icon tag for Twitter' do
126
-      icon = icon_for_service(:twitter)
143
+      icon = omniauth_provider_icon(:twitter)
127 144
       expect(icon).to be_html_safe
128 145
       elem = Nokogiri(icon).at('i.fa.fa-twitter')
129 146
       expect(elem).to be_a Nokogiri::XML::Element
130 147
     end
131 148
 
132 149
     it 'returns a correct icon tag for GitHub' do
133
-      icon = icon_for_service(:github)
150
+      icon = omniauth_provider_icon(:github)
134 151
       expect(icon).to be_html_safe
135 152
       elem = Nokogiri(icon).at('i.fa.fa-github')
136 153
       expect(elem).to be_a Nokogiri::XML::Element
137 154
     end
138 155
 
139 156
     it 'returns a correct icon tag for other services' do
140
-      icon = icon_for_service(:'37signals')
157
+      icon = omniauth_provider_icon(:'37signals')
141 158
       expect(icon).to be_html_safe
142 159
       elem = Nokogiri(icon).at('i.fa.fa-lock')
143 160
       expect(elem).to be_a Nokogiri::XML::Element
144 161
     end
145 162
   end
163
+
164
+  describe '#highlighted?' do
165
+    it 'understands hl=6-8' do
166
+      stub(params).[](:hl) { '6-8' }
167
+      expect((1..10).select { |i| highlighted?(i) }).to eq [6, 7, 8]
168
+    end
169
+
170
+    it 'understands hl=1,3-4,9' do
171
+      stub(params).[](:hl) { '1,3-4,9' }
172
+      expect((1..10).select { |i| highlighted?(i) }).to eq [1, 3, 4, 9]
173
+    end
174
+
175
+    it 'understands hl=8-' do
176
+      stub(params).[](:hl) { '8-' }
177
+      expect((1..10).select { |i| highlighted?(i) }).to eq [8, 9, 10]
178
+    end
179
+
180
+    it 'understands hl=-2' do
181
+      stub(params).[](:hl) { '-2' }
182
+      expect((1..10).select { |i| highlighted?(i) }).to eq [1, 2]
183
+    end
184
+
185
+    it 'understands hl=-' do
186
+      stub(params).[](:hl) { '-' }
187
+      expect((1..10).select { |i| highlighted?(i) }).to eq [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
188
+    end
189
+
190
+    it 'is OK with no hl' do
191
+      stub(params).[](:hl) { nil }
192
+      expect((1..10).select { |i| highlighted?(i) }).to be_empty
193
+    end
194
+  end
146 195
 end

+ 3 - 3
spec/lib/huginn_scheduler_spec.rb

@@ -86,11 +86,11 @@ describe Rufus::Scheduler do
86 86
 
87 87
     stub.any_instance_of(Agents::SchedulerAgent).second_precision_enabled { true }
88 88
 
89
-    @agent1 = Agents::SchedulerAgent.new(name: 'Scheduler 1', options: { schedule: '*/1 * * * * *' }).tap { |a|
89
+    @agent1 = Agents::SchedulerAgent.new(name: 'Scheduler 1', options: { action: 'run', schedule: '*/1 * * * * *' }).tap { |a|
90 90
       a.user = users(:bob)
91 91
       a.save!
92 92
     }
93
-    @agent2 = Agents::SchedulerAgent.new(name: 'Scheduler 2', options: { schedule: '*/1 * * * * *' }).tap { |a|
93
+    @agent2 = Agents::SchedulerAgent.new(name: 'Scheduler 2', options: { action: 'run', schedule: '*/1 * * * * *' }).tap { |a|
94 94
       a.user = users(:bob)
95 95
       a.save!
96 96
     }
@@ -107,7 +107,7 @@ describe Rufus::Scheduler do
107 107
     it 'registers active SchedulerAgents' do
108 108
       @scheduler.schedule_scheduler_agents
109 109
 
110
-      expect(@scheduler.scheduler_agent_jobs.map(&:scheduler_agent)).to eq([@agent1, @agent2])
110
+      expect(@scheduler.scheduler_agent_jobs.map(&:scheduler_agent).sort_by(&:id)).to eq([@agent1, @agent2])
111 111
     end
112 112
 
113 113
     it 'unregisters disabled SchedulerAgents' do

+ 2 - 2
spec/models/agent_log_spec.rb

@@ -44,9 +44,9 @@ describe AgentLog do
44 44
 
45 45
   it "truncates message to a reasonable length" do
46 46
     log = AgentLog.new(:agent => agents(:jane_website_agent), :level => 3)
47
-    log.message = "a" * 3000
47
+    log.message = "a" * 11_000
48 48
     log.save!
49
-    expect(log.message.length).to eq(2048)
49
+    expect(log.message.length).to eq(10_000)
50 50
   end
51 51
 
52 52
   describe "#log_for_agent" do

+ 17 - 12
spec/models/agent_spec.rb

@@ -561,12 +561,15 @@ describe Agent do
561 561
 
562 562
     describe "cleaning up now-expired events" do
563 563
       before do
564
-        @agent = Agents::SomethingSource.new(:name => "something")
565
-        @agent.keep_events_for = 5
566
-        @agent.user = users(:bob)
567
-        @agent.save!
568
-        @event = @agent.create_event :payload => { "hello" => "world" }
569
-        expect(@event.expires_at.to_i).to be_within(2).of(5.days.from_now.to_i)
564
+        @time = "2014-01-01 01:00:00 +00:00"
565
+        time_travel_to @time do
566
+          @agent = Agents::SomethingSource.new(:name => "something")
567
+          @agent.keep_events_for = 5
568
+          @agent.user = users(:bob)
569
+          @agent.save!
570
+          @event = @agent.create_event :payload => { "hello" => "world" }
571
+          expect(@event.expires_at.to_i).to be_within(2).of(5.days.from_now.to_i)
572
+        end
570 573
       end
571 574
 
572 575
       describe "when keep_events_for has not changed" do
@@ -584,12 +587,14 @@ describe Agent do
584 587
 
585 588
       describe "when keep_events_for is changed" do
586 589
         it "updates events' expires_at" do
587
-          expect {
588
-            @agent.options[:foo] = "bar1"
589
-            @agent.keep_events_for = 3
590
-            @agent.save!
591
-          }.to change { @event.reload.expires_at }
592
-          expect(@event.expires_at.to_i).to be_within(2).of(3.days.from_now.to_i)
590
+          time_travel_to @time do
591
+            expect {
592
+                @agent.options[:foo] = "bar1"
593
+                @agent.keep_events_for = 3
594
+                @agent.save!
595
+            }.to change { @event.reload.expires_at }
596
+            expect(@event.expires_at.to_i).to be_within(2).of(3.days.from_now.to_i)
597
+          end
593 598
         end
594 599
 
595 600
         it "updates events relative to their created_at" do

+ 28 - 5
spec/models/agents/basecamp_agent_spec.rb

@@ -5,8 +5,21 @@ describe Agents::BasecampAgent do
5 5
   it_behaves_like Oauthable
6 6
 
7 7
   before(:each) do
8
-    stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
9
-    stub_request(:get, /02:00$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"})
8
+    stub_request(:get, /events.json$/).to_return(
9
+      :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")),
10
+      :status => 200,
11
+      :headers => {"Content-Type" => "text/json"}
12
+    )
13
+    stub_request(:get, /projects.json$/).to_return(
14
+      :body => JSON.dump([{name: 'test', id: 1234},{name: 'test1', id: 1235}]),
15
+      :status => 200,
16
+      :headers => {"Content-Type" => "text/json"}
17
+    )
18
+    stub_request(:get, /02:00$/).to_return(
19
+      :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")),
20
+      :status => 200,
21
+      :headers => {"Content-Type" => "text/json"}
22
+    )
10 23
     @valid_params = { :project_id => 6789 }
11 24
 
12 25
     @checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params)
@@ -32,10 +45,13 @@ describe Agents::BasecampAgent do
32 45
       expect(@checker.send(:request_options)).to eq({:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => 'Bearer "1234token"'}})
33 46
     end
34 47
 
35
-    it "should generate the currect request url" do
36
-      expect(@checker.send(:request_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json")
48
+    it "should generate the correct events url" do
49
+      expect(@checker.send(:events_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json")
37 50
     end
38 51
 
52
+    it "should generate the correct projects url" do
53
+      expect(@checker.send(:projects_url)).to eq("https://basecamp.com/12345/api/v1/projects.json")
54
+    end
39 55
 
40 56
     it "should not provide the since attribute on first run" do
41 57
       expect(@checker.send(:query_parameters)).to eq({})
@@ -48,6 +64,13 @@ describe Agents::BasecampAgent do
48 64
       expect(@checker.reload.send(:query_parameters)).to eq({:query => {:since => time}})
49 65
     end
50 66
   end
67
+
68
+  describe "#complete_project_id" do
69
+    it "should return a array of hashes" do
70
+      expect(@checker.complete_project_id).to eq [{text: 'test (1234)', id: 1234}, {text: 'test1 (1235)', id: 1235}]
71
+    end
72
+  end
73
+
51 74
   describe "#check" do
52 75
     it "should not emit events on its first run" do
53 76
       expect { @checker.check }.to change { Event.count }.by(0)
@@ -60,7 +83,7 @@ describe Agents::BasecampAgent do
60 83
   end
61 84
 
62 85
   describe "#working?" do
63
-    it "it is working when at least one event was emited" do
86
+    it "it is working when at least one event was emitted" do
64 87
       expect(@checker).not_to be_working
65 88
       @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00'
66 89
       @checker.check

+ 42 - 0
spec/models/agents/commander_agent_spec.rb

@@ -0,0 +1,42 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::CommanderAgent do
4
+  let(:valid_params) {
5
+    {
6
+      name: 'Example',
7
+      schedule: 'every_1h',
8
+      options: {
9
+        'action' => 'run',
10
+      },
11
+    }
12
+  }
13
+
14
+  let(:agent) {
15
+    described_class.create!(valid_params) { |agent|
16
+      agent.user = users(:bob)
17
+    }
18
+  }
19
+
20
+  it_behaves_like AgentControllerConcern
21
+
22
+  describe "check!" do
23
+    it "should command targets" do
24
+      stub(agent).control!.once { nil }
25
+      agent.check!
26
+    end
27
+  end
28
+
29
+  describe "receive_events" do
30
+    it "should command targets" do
31
+      stub(agent).control!.once { nil }
32
+
33
+      event = Event.new
34
+      event.agent = agents(:bob_rain_notifier_agent)
35
+      event.payload = {
36
+        'url' => 'http://xkcd.com',
37
+        'link' => 'Random',
38
+      }
39
+      agent.receive([event])
40
+    end
41
+  end
42
+end

+ 74 - 0
spec/models/agents/dropbox_file_url_agent_spec.rb

@@ -0,0 +1,74 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::DropboxFileUrlAgent do
4
+  before(:each) do
5
+    @agent = Agents::DropboxFileUrlAgent.new(
6
+      name: 'dropbox file to url',
7
+      options: {}
8
+    )
9
+    @agent.user = users(:bob)
10
+    @agent.service = services(:generic)
11
+    @agent.save!
12
+  end
13
+
14
+  it 'cannot be scheduled' do
15
+    expect(@agent.cannot_be_scheduled?).to eq true
16
+  end
17
+
18
+  it 'has agent description' do
19
+    expect(@agent.description).to_not be_nil
20
+  end
21
+
22
+  it 'has event description' do
23
+    expect(@agent.event_description).to_not be_nil
24
+  end
25
+
26
+  describe "#receive" do
27
+
28
+    let(:first_dropbox_url_payload)  { { 'url' => 'http://dropbox.com/first/path/url' } }
29
+    let(:second_dropbox_url_payload) { { 'url' => 'http://dropbox.com/second/path/url' } }
30
+    let(:third_dropbox_url_payload)  { { 'url' => 'http://dropbox.com/third/path/url' } }
31
+
32
+    def create_event(payload)
33
+      event = Event.new(payload: payload)
34
+      event.agent = agents(:bob_manual_event_agent)
35
+      event.save!
36
+      event
37
+    end
38
+
39
+    before(:each) do
40
+      stub.proxy(Dropbox::API::Client).new do |api|
41
+        stub(api).find('/first/path')  { stub(Dropbox::API::File.new).direct_url { first_dropbox_url_payload } }
42
+        stub(api).find('/second/path') { stub(Dropbox::API::File.new).direct_url { second_dropbox_url_payload } }
43
+        stub(api).find('/third/path')  { stub(Dropbox::API::File.new).direct_url { third_dropbox_url_payload } }
44
+      end
45
+    end
46
+
47
+    context 'with a single path' do
48
+
49
+      before(:each) { @event = create_event(paths: '/first/path') }
50
+
51
+      it 'creates one event with the temporary dropbox link' do
52
+        expect { @agent.receive([@event]) }.to change(Event, :count).by(1)
53
+        expect(Event.last.payload).to eq(first_dropbox_url_payload)
54
+      end
55
+
56
+    end
57
+
58
+    context 'with multiple comma-separated paths' do
59
+
60
+      before(:each) { @event = create_event(paths: '/first/path, /second/path, /third/path') }
61
+
62
+      it 'creates one event with the temporary dropbox link for each path' do
63
+        expect { @agent.receive([@event]) }.to change(Event, :count).by(3)
64
+        last_events = Event.last(3)
65
+        expect(last_events[0].payload).to eq(first_dropbox_url_payload)
66
+        expect(last_events[1].payload).to eq(second_dropbox_url_payload)
67
+        expect(last_events[2].payload).to eq(third_dropbox_url_payload)
68
+      end
69
+
70
+    end
71
+
72
+  end
73
+
74
+end

+ 173 - 0
spec/models/agents/dropbox_watch_agent_spec.rb

@@ -0,0 +1,173 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::DropboxWatchAgent do
4
+  before(:each) do
5
+    @agent = Agents::DropboxWatchAgent.new(
6
+      name: 'save to dropbox',
7
+      options: {
8
+        access_token: '70k3n',
9
+        dir_to_watch: '/my/dropbox/dir',
10
+        expected_update_period_in_days: 2
11
+      }
12
+    )
13
+    @agent.user = users(:bob)
14
+    @agent.service = services(:generic)
15
+    @agent.save!
16
+  end
17
+
18
+  it 'cannot receive events' do
19
+    expect(@agent.cannot_receive_events?).to eq true
20
+  end
21
+
22
+  it 'has agent description' do
23
+    expect(@agent.description).to_not be_nil
24
+  end
25
+
26
+  it 'has event description' do
27
+    expect(@agent.event_description).to_not be_nil
28
+  end
29
+
30
+  describe '#valid?' do
31
+    before(:each) { expect(@agent.valid?).to eq true }
32
+
33
+    it 'requires a "dir_to_watch"' do
34
+      @agent.options[:dir_to_watch] = nil
35
+      expect(@agent.valid?).to eq false
36
+    end
37
+
38
+    describe 'expected_update_period_in_days' do
39
+      it 'needs to be present' do
40
+        @agent.options[:expected_update_period_in_days] = nil
41
+        expect(@agent.valid?).to eq false
42
+      end
43
+
44
+      it 'needs to be a positive integer' do
45
+        @agent.options[:expected_update_period_in_days] = -1
46
+        expect(@agent.valid?).to eq false
47
+      end
48
+    end
49
+  end
50
+
51
+  describe '#check' do
52
+
53
+    let(:first_result) { [{ 'path' => '1.json', 'rev' => '1', 'modified' => '01-01-01' }] }
54
+
55
+    before(:each) do
56
+      stub.proxy(Dropbox::API::Client).new do |api|
57
+        stub(api).ls('/my/dropbox/dir') { first_result }
58
+      end
59
+    end
60
+
61
+    it 'saves the directory listing in its memory' do
62
+      @agent.check
63
+      expect(@agent.memory).to eq 'contents' => first_result
64
+    end
65
+
66
+    context 'first time' do
67
+
68
+      before(:each) { @agent.memory = {} }
69
+
70
+      it 'does not send any events' do
71
+        expect { @agent.check }.to_not change(Event, :count)
72
+      end
73
+
74
+    end
75
+
76
+    context 'subsequent calls' do
77
+
78
+      let(:second_result) { [{ 'path' => '2.json', 'rev' => '1', 'modified' => '02-02-02' }] }
79
+
80
+      before(:each) do
81
+        @agent.memory = { 'contents' => 'not_empty' }
82
+
83
+        stub.proxy(Dropbox::API::Client).new do |api|
84
+          stub(api).ls('/my/dropbox/dir') { second_result }
85
+        end
86
+      end
87
+
88
+      it 'sends an event upon a different directory listing' do
89
+        payload = { 'diff' => 'object as hash' }
90
+        stub.proxy(Agents::DropboxWatchAgent::DropboxDirDiff).new(@agent.memory['contents'], second_result) do |diff|
91
+          stub(diff).empty? { false }
92
+          stub(diff).to_hash { payload }
93
+        end
94
+        expect { @agent.check }.to change(Event, :count).by(1)
95
+        expect(Event.last.payload).to eq(payload)
96
+      end
97
+
98
+      it 'does not sent any events when there is no difference on the directory listing' do
99
+        stub.proxy(Agents::DropboxWatchAgent::DropboxDirDiff).new(@agent.memory['contents'], second_result) do |diff|
100
+          stub(diff).empty? { true }
101
+        end
102
+
103
+        expect { @agent.check }.to_not change(Event, :count)
104
+      end
105
+
106
+    end
107
+  end
108
+
109
+  describe Agents::DropboxWatchAgent::DropboxDirDiff do
110
+
111
+    let(:previous) { [
112
+      { 'path' => '1.json', 'rev' => '1' },
113
+      { 'path' => '2.json', 'rev' => '1' },
114
+      { 'path' => '3.json', 'rev' => '1' }
115
+    ] }
116
+
117
+    let(:current) { [
118
+      { 'path' => '1.json', 'rev' => '2' },
119
+      { 'path' => '3.json', 'rev' => '1' },
120
+      { 'path' => '4.json', 'rev' => '1' }
121
+    ] }
122
+
123
+    describe '#empty?' do
124
+
125
+      it 'is true when no differences are detected' do
126
+        diff = Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, previous)
127
+        expect(diff.empty?).to eq true
128
+      end
129
+
130
+      it 'is false when differences were detected' do
131
+        diff = Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, current)
132
+        expect(diff.empty?).to eq false
133
+      end
134
+
135
+    end
136
+
137
+    describe '#to_hash' do
138
+
139
+      subject(:diff_hash) { Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, current).to_hash }
140
+
141
+      it 'detects additions' do
142
+        expect(diff_hash[:added]).to eq [{ 'path' => '4.json', 'rev' => '1' }]
143
+      end
144
+
145
+      it 'detects removals' do
146
+        expect(diff_hash[:removed]).to eq [ { 'path' => '2.json', 'rev' => '1' } ]
147
+      end
148
+
149
+      it 'detects updates' do
150
+        expect(diff_hash[:updated]).to eq [ { 'path' => '1.json', 'rev' => '2' } ]
151
+      end
152
+
153
+      context 'when the previous value is not defined' do
154
+        it 'considers all additions' do
155
+          diff_hash = Agents::DropboxWatchAgent::DropboxDirDiff.new(nil, current).to_hash
156
+          expect(diff_hash[:added]).to eq current
157
+          expect(diff_hash[:removed]).to eq []
158
+          expect(diff_hash[:updated]).to eq []
159
+        end
160
+      end
161
+
162
+      context 'when the current value is not defined' do
163
+        it 'considers all removals' do
164
+          diff_hash = Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, nil).to_hash
165
+          expect(diff_hash[:added]).to eq []
166
+          expect(diff_hash[:removed]).to eq previous
167
+          expect(diff_hash[:updated]).to eq []
168
+        end
169
+      end
170
+    end
171
+  end
172
+
173
+end

+ 25 - 0
spec/models/agents/hipchat_agent_spec.rb

@@ -50,6 +50,31 @@ describe Agents::HipchatAgent do
50 50
     end
51 51
   end
52 52
 
53
+  describe "#validate_auth_token" do
54
+    it "should return true when valid" do
55
+      any_instance_of(HipChat::Client) do |klass|
56
+        stub(klass).rooms { true }
57
+      end
58
+      expect(@checker.validate_auth_token).to be true
59
+    end
60
+
61
+    it "should return false when invalid" do
62
+      any_instance_of(HipChat::Client) do |klass|
63
+        stub(klass).rooms { raise HipChat::UnknownResponseCode.new }
64
+      end
65
+      expect(@checker.validate_auth_token).to be false
66
+    end
67
+  end
68
+
69
+  describe "#complete_room_name" do
70
+    it "should return a array of hashes" do
71
+      any_instance_of(HipChat::Client) do |klass|
72
+        stub(klass).rooms { [OpenStruct.new(name: 'test'), OpenStruct.new(name: 'test1')] }
73
+      end
74
+      expect(@checker.complete_room_name).to eq [{text: 'test', id: 'test'},{text: 'test1', id: 'test1'}]
75
+    end
76
+  end
77
+
53 78
   describe "#receive" do
54 79
     it "send a message to the hipchat" do
55 80
       any_instance_of(HipChat::Room) do |obj|

+ 8 - 2
spec/models/agents/mqtt_agent_spec.rb

@@ -8,7 +8,6 @@ describe Agents::MqttAgent do
8 8
     @error_log = StringIO.new
9 9
 
10 10
     @server = MQTT::FakeServer.new(41234, '127.0.0.1')
11
-    @server.just_one = true
12 11
     @server.logger = Logger.new(@error_log)
13 12
     @server.logger.level = Logger::DEBUG
14 13
     @server.start
@@ -34,7 +33,14 @@ describe Agents::MqttAgent do
34 33
   end
35 34
 
36 35
   describe "#check" do
37
-    it "should check that initial run creates an event" do
36
+    it "should create events in the initial run" do
37
+      expect { @checker.check }.to change { Event.count }.by(2)
38
+    end
39
+
40
+    it "should ignore retained messages that are previously received" do
41
+      expect { @checker.check }.to change { Event.count }.by(2)
42
+      expect { @checker.check }.to change { Event.count }.by(1)
43
+      expect { @checker.check }.to change { Event.count }.by(1)
38 44
       expect { @checker.check }.to change { Event.count }.by(2)
39 45
     end
40 46
   end

+ 4 - 0
spec/models/agents/rss_agent_spec.rb

@@ -55,6 +55,10 @@ describe Agents::RssAgent do
55 55
       expect {
56 56
         agent.check
57 57
       }.to change { agent.events.count }.by(20)
58
+
59
+      event = agent.events.last
60
+      expect(event.payload['url']).to eq("https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0")
61
+      expect(event.payload['urls']).to eq(["https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0"])
58 62
     end
59 63
 
60 64
     it "should track ids and not re-emit the same item when seen again" do

+ 59 - 105
spec/models/agents/scheduler_agent_spec.rb

@@ -1,96 +1,68 @@
1 1
 require 'spec_helper'
2 2
 
3 3
 describe Agents::SchedulerAgent do
4
-  before do
5
-    @agent = Agents::SchedulerAgent.new(name: 'Example', options: { 'schedule' => '0 * * * *' })
6
-    @agent.user = users(:bob)
7
-    @agent.save
8
-  end
4
+  let(:valid_params) {
5
+    {
6
+      name: 'Example',
7
+      options: {
8
+        'action' => 'run',
9
+        'schedule' => '0 * * * *'
10
+      },
11
+    }
12
+  }
13
+
14
+  let(:agent) {
15
+    described_class.create!(valid_params) { |agent|
16
+      agent.user = users(:bob)
17
+    }
18
+  }
19
+
20
+  it_behaves_like AgentControllerConcern
9 21
 
10 22
   describe "validation" do
11
-    it "should validate action" do
12
-      ['run', 'enable', 'disable', '', nil].each { |action|
13
-        @agent.options['action'] = action
14
-        expect(@agent).to be_valid
15
-      }
16
-
17
-      ['delete', 1, true].each { |action|
18
-        @agent.options['action'] = action
19
-        expect(@agent).not_to be_valid
20
-      }
21
-    end
22
-
23 23
     it "should validate schedule" do
24
-      expect(@agent).to be_valid
24
+      expect(agent).to be_valid
25 25
 
26
-      @agent.options.delete('schedule')
27
-      expect(@agent).not_to be_valid
26
+      agent.options.delete('schedule')
27
+      expect(agent).not_to be_valid
28 28
 
29
-      @agent.options['schedule'] = nil
30
-      expect(@agent).not_to be_valid
29
+      agent.options['schedule'] = nil
30
+      expect(agent).not_to be_valid
31 31
 
32
-      @agent.options['schedule'] = ''
33
-      expect(@agent).not_to be_valid
32
+      agent.options['schedule'] = ''
33
+      expect(agent).not_to be_valid
34 34
 
35
-      @agent.options['schedule'] = '0'
36
-      expect(@agent).not_to be_valid
35
+      agent.options['schedule'] = '0'
36
+      expect(agent).not_to be_valid
37 37
 
38
-      @agent.options['schedule'] = '*/15 * * * * * *'
39
-      expect(@agent).not_to be_valid
38
+      agent.options['schedule'] = '*/15 * * * * * *'
39
+      expect(agent).not_to be_valid
40 40
 
41
-      @agent.options['schedule'] = '*/1 * * * *'
42
-      expect(@agent).to be_valid
41
+      agent.options['schedule'] = '*/1 * * * *'
42
+      expect(agent).to be_valid
43 43
 
44
-      @agent.options['schedule'] = '*/1 * * *'
45
-      expect(@agent).not_to be_valid
44
+      agent.options['schedule'] = '*/1 * * *'
45
+      expect(agent).not_to be_valid
46 46
 
47
-      stub(@agent).second_precision_enabled { true }
48
-      @agent.options['schedule'] = '*/15 * * * * *'
49
-      expect(@agent).to be_valid
47
+      stub(agent).second_precision_enabled { true }
48
+      agent.options['schedule'] = '*/15 * * * * *'
49
+      expect(agent).to be_valid
50 50
 
51
-      stub(@agent).second_precision_enabled { false }
52
-      @agent.options['schedule'] = '*/10 * * * * *'
53
-      expect(@agent).not_to be_valid
51
+      stub(agent).second_precision_enabled { false }
52
+      agent.options['schedule'] = '*/10 * * * * *'
53
+      expect(agent).not_to be_valid
54 54
 
55
-      @agent.options['schedule'] = '5/30 * * * * *'
56
-      expect(@agent).not_to be_valid
55
+      agent.options['schedule'] = '5/30 * * * * *'
56
+      expect(agent).not_to be_valid
57 57
 
58
-      @agent.options['schedule'] = '*/15 * * * * *'
59
-      expect(@agent).to be_valid
58
+      agent.options['schedule'] = '*/15 * * * * *'
59
+      expect(agent).to be_valid
60 60
 
61
-      @agent.options['schedule'] = '15,45 * * * * *'
62
-      expect(@agent).to be_valid
61
+      agent.options['schedule'] = '15,45 * * * * *'
62
+      expect(agent).to be_valid
63 63
 
64
-      @agent.options['schedule'] = '0 * * * * *'
65
-      expect(@agent).to be_valid
66
-    end
67
-  end
68
-
69
-  describe 'control_action' do
70
-    it "should be one of the supported values" do
71
-      ['run', '', nil].each { |action|
72
-        @agent.options['action'] = action
73
-        expect(@agent.control_action).to eq('run')
74
-      }
75
-
76
-      ['enable', 'disable'].each { |action|
77
-        @agent.options['action'] = action
78
-        expect(@agent.control_action).to eq(action)
79
-      }
80
-    end
81
-
82
-    it "cannot be 'run' if any of the control targets cannot be scheduled" do
83
-      expect(@agent.control_action).to eq('run')
84
-      @agent.control_targets = [agents(:bob_rain_notifier_agent)]
85
-      expect(@agent).not_to be_valid
86
-    end
87
-
88
-    it "can be 'enable' or 'disable' no matter if control targets can be scheduled or not" do
89
-      ['enable', 'disable'].each { |action|
90
-        @agent.options['action'] = action
91
-        @agent.control_targets = [agents(:bob_rain_notifier_agent)]
92
-        expect(@agent).to be_valid
93
-      }
64
+      agent.options['schedule'] = '0 * * * * *'
65
+      expect(agent).to be_valid
94 66
     end
95 67
   end
96 68
 
@@ -98,43 +70,25 @@ describe Agents::SchedulerAgent do
98 70
     it "should delete memory['scheduled_at'] if and only if options is changed" do
99 71
       time = Time.now.to_i
100 72
 
101
-      @agent.memory['scheduled_at'] = time
102
-      @agent.save
103
-      expect(@agent.memory['scheduled_at']).to eq(time)
73
+      agent.memory['scheduled_at'] = time
74
+      agent.save
75
+      expect(agent.memory['scheduled_at']).to eq(time)
104 76
 
105
-      @agent.memory['scheduled_at'] = time
106
-      # Currently @agent.options[]= is not detected
107
-      @agent.options = { 'schedule' => '*/5 * * * *' }
108
-      @agent.save
109
-      expect(@agent.memory['scheduled_at']).to be_nil
77
+      agent.memory['scheduled_at'] = time
78
+      # Currently agent.options[]= is not detected
79
+      agent.options = {
80
+        'action' => 'run',
81
+        'schedule' => '*/5 * * * *'
82
+      }
83
+      agent.save
84
+      expect(agent.memory['scheduled_at']).to be_nil
110 85
     end
111 86
   end
112 87
 
113 88
   describe "check!" do
114 89
     it "should control targets" do
115
-      control_targets = [agents(:bob_website_agent), agents(:bob_weather_agent)]
116
-      @agent.control_targets = control_targets
117
-      @agent.save!
118
-
119
-      control_target_ids = control_targets.map(&:id)
120
-      stub(Agent).async_check(anything) { |id|
121
-        control_target_ids.delete(id)
122
-      }
123
-
124
-      @agent.check!
125
-      expect(control_target_ids).to be_empty
126
-
127
-      @agent.options['action'] = 'disable'
128
-      @agent.save!
129
-
130
-      @agent.check!
131
-      control_targets.all? { |control_target| control_target.disabled? }
132
-
133
-      @agent.options['action'] = 'enable'
134
-      @agent.save!
135
-
136
-      @agent.check!
137
-      control_targets.all? { |control_target| !control_target.disabled? }
90
+      stub(agent).control!.once { nil }
91
+      agent.check!
138 92
     end
139 93
   end
140 94
 end

+ 1 - 1
spec/models/service_spec.rb

@@ -15,7 +15,7 @@ describe Service do
15 15
       expect(@service.global).to eq(false)
16 16
     end
17 17
 
18
-    it "disconnects agents and disables them if the previously global service is made private again", focus: true do
18
+    it "disconnects agents and disables them if the previously global service is made private again" do
19 19
       agent = agents(:bob_basecamp_agent)
20 20
       jane_agent = agents(:jane_basecamp_agent)
21 21
 

+ 40 - 0
spec/presenters/form_configurable_agent_presenter_spec.rb

@@ -0,0 +1,40 @@
1
+require 'spec_helper'
2
+
3
+describe FormConfigurableAgentPresenter do
4
+  class FormConfigurableAgentPresenterAgent < Agent
5
+    include FormConfigurable
6
+
7
+    form_configurable :string, roles: :validatable
8
+    form_configurable :text, type: :text, roles: :completable
9
+    form_configurable :boolean, type: :boolean
10
+    form_configurable :array, type: :array, values: [1, 2, 3]
11
+  end
12
+
13
+  before(:all) do
14
+    @presenter = FormConfigurableAgentPresenter.new(FormConfigurableAgentPresenterAgent.new, ActionController::Base.new.view_context)
15
+  end
16
+
17
+  it "works for the type :string" do
18
+    expect(@presenter.option_field_for(:string)).to(
19
+      have_tag('input', with: {:'data-attribute' => 'string', role: 'validatable form-configurable', type: 'text', name: 'agent[options][string]'})
20
+    )
21
+  end
22
+
23
+  it "works for the type :text" do
24
+    expect(@presenter.option_field_for(:text)).to(
25
+      have_tag('textarea', with: {:'data-attribute' => 'text', role: 'completable form-configurable', name: 'agent[options][text]'})
26
+    )
27
+  end
28
+
29
+  it "works for the type :boolean" do
30
+    expect(@presenter.option_field_for(:boolean)).to(
31
+      have_tag('input', with: {:'data-attribute' => 'boolean', role: 'form-configurable', name: 'agent[options][boolean_radio]', type: 'radio'})
32
+    )
33
+  end
34
+
35
+  it "works for the type :array" do
36
+    expect(@presenter.option_field_for(:array)).to(
37
+      have_tag('input', with: {:'data-attribute' => 'array', role: 'completable form-configurable', type: 'text', name: 'agent[options][array]'})
38
+    )
39
+  end
40
+end

+ 30 - 11
spec/support/fake_mqtt_server.rb

@@ -52,7 +52,9 @@ class MQTT::FakeServer
52 52
     @port = @socket.addr[1]
53 53
     @thread ||= Thread.new do
54 54
       logger.info "Started a fake MQTT server on #{@address}:#{@port}"
55
+      @times = 0
55 56
       loop do
57
+        @times += 1
56 58
         # Wait for a client to connect
57 59
         client = @socket.accept
58 60
         @pings_received = 0
@@ -103,16 +105,33 @@ class MQTT::FakeServer
103 105
             :granted_qos => 0
104 106
           )
105 107
           topic = packet.topics[0][0]
106
-          client.write MQTT::Packet::Publish.new(
107
-            :topic => topic,
108
-            :payload => "hello #{topic}",
109
-            :retain => true
110
-          )
111
-          client.write MQTT::Packet::Publish.new(
112
-            :topic => topic,
113
-            :payload => "did you know about #{topic}",
114
-            :retain => true
115
-          )
108
+          case @times
109
+          when 1, ->x { x >= 3 }
110
+            # Deliver retained messages
111
+            client.write MQTT::Packet::Publish.new(
112
+              :topic => topic,
113
+              :payload => "did you know about #{topic}",
114
+              :retain => true
115
+            )
116
+            client.write MQTT::Packet::Publish.new(
117
+              :topic => topic,
118
+              :payload => "hello #{topic}",
119
+              :retain => true
120
+            )
121
+          when 2
122
+            # Deliver a still retained message
123
+            client.write MQTT::Packet::Publish.new(
124
+              :topic => topic,
125
+              :payload => "hello #{topic}",
126
+              :retain => true
127
+            )
128
+            # Deliver a fresh message
129
+            client.write MQTT::Packet::Publish.new(
130
+              :topic => topic,
131
+              :payload => "did you know about #{topic}",
132
+              :retain => false
133
+            )
134
+          end
116 135
 
117 136
         when MQTT::Packet::Pingreq
118 137
           client.write MQTT::Packet::Pingresp.new
@@ -134,4 +153,4 @@ if __FILE__ == $0
134 153
   server = MQTT::FakeServer.new(MQTT::DEFAULT_PORT)
135 154
   server.logger.level = Logger::DEBUG
136 155
   server.run
137
-end
156
+end

+ 111 - 0
spec/support/shared_examples/agent_controller_concern.rb

@@ -0,0 +1,111 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for AgentControllerConcern do
4
+  describe "preconditions" do
5
+    it "must be satisfied for these shared examples" do
6
+      expect(agent.user).to eq(users(:bob))
7
+      expect(agent.control_action).to eq('run')
8
+    end
9
+  end
10
+
11
+  describe "validation" do
12
+    describe "of action" do
13
+      it "should allow certain values" do
14
+        ['run', 'enable', 'disable', '{{ action }}'].each { |action|
15
+          agent.options['action'] = action
16
+          expect(agent).to be_valid
17
+        }
18
+      end
19
+
20
+      it "should disallow obviously bad values" do
21
+        ['delete', nil, 1, true].each { |action|
22
+          agent.options['action'] = action
23
+          expect(agent).not_to be_valid
24
+        }
25
+      end
26
+
27
+      it "should accept 'run' if all target agents are schedulable" do
28
+        agent.control_targets = [agents(:bob_website_agent)]
29
+        expect(agent).to be_valid
30
+      end
31
+
32
+      it "should reject 'run' if targets include an unschedulable agent" do
33
+        agent.control_targets = [agents(:bob_rain_notifier_agent)]
34
+        expect(agent).not_to be_valid
35
+      end
36
+
37
+      it "should not reject 'enable' or 'disable' no matter if targets include an unschedulable agent" do
38
+        ['enable', 'disable'].each { |action|
39
+          agent.options['action'] = action
40
+          agent.control_targets = [agents(:bob_rain_notifier_agent)]
41
+          expect(agent).to be_valid
42
+        }
43
+      end
44
+    end
45
+  end
46
+
47
+  describe 'control_action' do
48
+    it "returns options['action']" do
49
+      expect(agent.control_action).to eq('run')
50
+
51
+      ['run', 'enable', 'disable'].each { |action|
52
+        agent.options['action'] = action
53
+        expect(agent.control_action).to eq(action)
54
+      }
55
+    end
56
+
57
+    it "returns the result of interpolation" do
58
+      expect(agent.control_action).to eq('run')
59
+
60
+      agent.options['action'] = '{{ "enable" }}'
61
+      expect(agent.control_action).to eq('enable')
62
+    end
63
+  end
64
+
65
+  describe "control!" do
66
+    before do
67
+      agent.control_targets = [agents(:bob_website_agent), agents(:bob_weather_agent)]
68
+      agent.save!
69
+    end
70
+
71
+    it "should run targets" do
72
+      control_target_ids = agent.control_targets.map(&:id)
73
+      stub(Agent).async_check(anything) { |id|
74
+        control_target_ids.delete(id)
75
+      }
76
+
77
+      agent.control!
78
+      expect(control_target_ids).to be_empty
79
+    end
80
+
81
+    it "should not run disabled targets" do
82
+      control_target_ids = agent.control_targets.map(&:id)
83
+      stub(Agent).async_check(anything) { |id|
84
+        control_target_ids.delete(id)
85
+      }
86
+
87
+      agent.control_targets.last.update!(disabled: true)
88
+
89
+      agent.control!
90
+      expect(control_target_ids).to eq [agent.control_targets.last.id]
91
+    end
92
+
93
+    it "should enable targets" do
94
+      agent.options['action'] = 'disable'
95
+      agent.save!
96
+      agent.control_targets.first.update!(disabled: true)
97
+
98
+      agent.control!
99
+      expect(agent.control_targets.reload).to all(be_disabled)
100
+    end
101
+
102
+    it "should disable targets" do
103
+      agent.options['action'] = 'enable'
104
+      agent.save!
105
+      agent.control_targets.first.update!(disabled: true)
106
+
107
+      agent.control!
108
+      expect(agent.control_targets.reload).to all(satisfy { |a| !a.disabled? })
109
+    end
110
+  end
111
+end

+ 40 - 0
vendor/assets/javascripts/jquery.serializeObject.js

@@ -0,0 +1,40 @@
1
+//
2
+// Use internal $.serializeArray to get list of form elements which is
3
+// consistent with $.serialize
4
+//
5
+// From version 2.0.0, $.serializeObject will stop converting [name] values
6
+// to camelCase format. This is *consistent* with other serialize methods:
7
+//
8
+//   - $.serialize
9
+//   - $.serializeArray
10
+//
11
+// If you require camel casing, you can either download version 1.0.4 or map
12
+// them yourself.
13
+//
14
+
15
+(function($){
16
+  $.fn.serializeObject = function () {
17
+    "use strict";
18
+
19
+    var result = {};
20
+    var extend = function (i, element) {
21
+      var node = result[element.name];
22
+
23
+  // If node with same name exists already, need to convert it to an array as it
24
+  // is a multi-value field (i.e., checkboxes)
25
+
26
+      if ('undefined' !== typeof node && node !== null) {
27
+        if ($.isArray(node)) {
28
+          node.push(element.value);
29
+        } else {
30
+          result[element.name] = [node, element.value];
31
+        }
32
+      } else {
33
+        result[element.name] = element.value;
34
+      }
35
+    };
36
+
37
+    $.each(this.serializeArray(), extend);
38
+    return result;
39
+  };
40
+})(jQuery);